From 2c8a5586a183e63bc936039f77da5e7bf0443745 Mon Sep 17 00:00:00 2001 From: whereareiam Date: Wed, 28 Oct 2026 17:18:17 +0100 Subject: [PATCH 1/4] chore(build): one source per shipped file, no gitea reference files gitea/ held 15 upstream Gitea files that update:gitea refreshed from go-gitea at the tag of the theme's own version, a number that follows this theme's releases and not Gitea's, and nothing read them. They go with the script, its proxy helper and the undici dependency only it used. Vite copied public/ into dist/ and the assembler then copied the page stylesheets into the Forgejo tree twice, once from public/ and once from dist/. Vite now leaves public/ alone (publicDir: false), so dist/ holds only the themes and the assembler copies public/ itself. The assembler also copies every stylesheet in styles/pages instead of a hand-written list; a test checks that each one is linked from component_assets.tmpl, so a new page stylesheet only needs that link. dist/forgejo is byte for byte the same as before. The preview ran its one-off chown and cleanup containers on Forgejo 16.0.3 while Compose ran 17.0.0; the constant now names 17.0.0 and a test keeps it equal to the Compose service's image. Co-Authored-By: Claude Opus 5.5 --- .gitattributes | 1 - ARCHITECTURE.md | 4 +- CONTRIBUTING.md | 1 - dev/preview/config/constants.test.ts | 12 + dev/preview/config/constants.ts | 2 +- gitea/templates/base/head_navbar.tmpl | 182 ---------- gitea/templates/base/head_navbar_icons.tmpl | 20 -- gitea/templates/repo/commit_sign_badge.tmpl | 81 ----- gitea/templates/repo/commits_list.tmpl | 80 ----- gitea/templates/repo/view_content.tmpl | 136 ------- gitea/templates/repo/view_list.tmpl | 67 ---- ...eme-gitea-auto-protanopia-deuteranopia.css | 8 - .../themes/theme-gitea-auto-tritanopia.css | 8 - gitea/web_src/css/themes/theme-gitea-auto.css | 7 - ...eme-gitea-dark-protanopia-deuteranopia.css | 19 - .../themes/theme-gitea-dark-tritanopia.css | 15 - gitea/web_src/css/themes/theme-gitea-dark.css | 337 ------------------ ...me-gitea-light-protanopia-deuteranopia.css | 19 - .../themes/theme-gitea-light-tritanopia.css | 15 - .../web_src/css/themes/theme-gitea-light.css | 313 ---------------- package.json | 2 - scripts/assembleForgejo.ts | 25 +- scripts/updateGitea.ts | 71 ---- scripts/utils.ts | 12 - src/build-output.test.ts | 46 ++- vite.config.ts | 3 +- 26 files changed, 50 insertions(+), 1436 deletions(-) create mode 100644 dev/preview/config/constants.test.ts delete mode 100644 gitea/templates/base/head_navbar.tmpl delete mode 100644 gitea/templates/base/head_navbar_icons.tmpl delete mode 100644 gitea/templates/repo/commit_sign_badge.tmpl delete mode 100644 gitea/templates/repo/commits_list.tmpl delete mode 100644 gitea/templates/repo/view_content.tmpl delete mode 100644 gitea/templates/repo/view_list.tmpl delete mode 100644 gitea/web_src/css/themes/theme-gitea-auto-protanopia-deuteranopia.css delete mode 100644 gitea/web_src/css/themes/theme-gitea-auto-tritanopia.css delete mode 100644 gitea/web_src/css/themes/theme-gitea-auto.css delete mode 100644 gitea/web_src/css/themes/theme-gitea-dark-protanopia-deuteranopia.css delete mode 100644 gitea/web_src/css/themes/theme-gitea-dark-tritanopia.css delete mode 100644 gitea/web_src/css/themes/theme-gitea-dark.css delete mode 100644 gitea/web_src/css/themes/theme-gitea-light-protanopia-deuteranopia.css delete mode 100644 gitea/web_src/css/themes/theme-gitea-light-tritanopia.css delete mode 100644 gitea/web_src/css/themes/theme-gitea-light.css delete mode 100644 scripts/updateGitea.ts 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..a68bd340 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -22,7 +22,8 @@ routes; `components/project` contains the project list shared by repository, org 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. +development and packaging. Every stylesheet in `styles/pages` ships as `css/components/pages/.css`; a new one only +needs its `` in `templates/base/component_assets.tmpl`. ## Architecture Layers @@ -454,7 +455,6 @@ outputs static CSS strings. ├── 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 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 196ea8a2..ee704b80 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -19,7 +19,6 @@ | primer | Auto-generated Primer design tokens | | scripts | Build and utility scripts | | templates | Gitea template overrides (optional) | -| gitea | Upstream Gitea reference files | | screenshots | Theme screenshots | ## Contribution Notes diff --git a/dev/preview/config/constants.test.ts b/dev/preview/config/constants.test.ts new file mode 100644 index 00000000..a1e154dd --- /dev/null +++ b/dev/preview/config/constants.test.ts @@ -0,0 +1,12 @@ +import { readFileSync } from "node:fs"; +import { fileURLToPath } from "node:url"; +import { expect, it } from "vitest"; + +import { COMPOSE } from "./constants.ts"; + +it("runs its one-off containers on the image of the Compose service", () => { + // The ownership repair and database cleanup start the image by name; another version would be pulled beside it. + const compose = readFileSync(fileURLToPath(new URL(`../../../${COMPOSE.file}`, import.meta.url)), "utf-8"); + const service = compose.slice(compose.indexOf(` ${COMPOSE.service}:\n`)); + expect(service.match(/^ {4}image: (\S+)$/m)?.[1]).toBe(COMPOSE.image); +}); diff --git a/dev/preview/config/constants.ts b/dev/preview/config/constants.ts index 32bba5bb..78931200 100644 --- a/dev/preview/config/constants.ts +++ b/dev/preview/config/constants.ts @@ -10,7 +10,7 @@ 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", + image: "code.forgejo.org/forgejo/forgejo:17.0.0-rootless", } as const; export const PREVIEW = { diff --git a/gitea/templates/base/head_navbar.tmpl b/gitea/templates/base/head_navbar.tmpl deleted file mode 100644 index cc7e4e67..00000000 --- a/gitea/templates/base/head_navbar.tmpl +++ /dev/null @@ -1,182 +0,0 @@ - -{{template "base/head_banner"}} diff --git a/gitea/templates/base/head_navbar_icons.tmpl b/gitea/templates/base/head_navbar_icons.tmpl deleted file mode 100644 index 019e78cb..00000000 --- a/gitea/templates/base/head_navbar_icons.tmpl +++ /dev/null @@ -1,20 +0,0 @@ -{{- $itemExtraClass := .ItemExtraClass -}} -{{- $data := .PageGlobalData -}} -{{if and $data $data.IsSigned}}{{/* data may not exist, for example: rendering 503 page before the PageGlobalData middleware */}} - {{- $activeStopwatch := call $data.GetActiveStopwatch -}} - {{- $notificationUnreadCount := call $data.GetNotificationUnreadCount -}} - {{if $activeStopwatch}} - -
- {{svg "octicon-stopwatch"}} - -
-
- {{end}} - -
- {{svg "octicon-bell"}} - {{$notificationUnreadCount}} -
-
-{{end}} diff --git a/gitea/templates/repo/commit_sign_badge.tmpl b/gitea/templates/repo/commit_sign_badge.tmpl deleted file mode 100644 index bf8185fd..00000000 --- a/gitea/templates/repo/commit_sign_badge.tmpl +++ /dev/null @@ -1,81 +0,0 @@ -{{/* Template attributes: -* Commit -* CommitBaseLink -* CommitSignVerification -If you'd like to modify this template, you could test it on the devtest page. -ATTENTION: this template could be re-rendered many times (on the graph and commit list page), -so this template should be kept as small as possible, DO NOT put large components like modal/dialog into it. -*/}} -{{- $commit := $.Commit -}} -{{- $commitBaseLink := $.CommitBaseLink -}} -{{- $verification := $.CommitSignVerification -}}{{- /* asymkey.CommitVerification */ -}} - -{{- $extraClass := "" -}} -{{- $verified := false -}} -{{- $signingUser := NIL -}} -{{- $signingEmail := "" -}} -{{- $msgReasonPrefix := "" -}} -{{- $msgReason := "" -}} -{{- $msgSigningKey := "" -}} - -{{- if $verification -}} - {{- $signingUser = $verification.SigningUser -}} - {{- $signingEmail = $verification.SigningEmail -}} - {{- $extraClass = print $extraClass " commit-is-signed" -}} - {{- if $verification.Verified -}} - {{- /* reason is "{name} / {key-id}" */ -}} - {{- $msgReason = $verification.Reason -}} - {{- $verified = true -}} - {{- if eq $verification.TrustStatus "trusted" -}} - {{- $extraClass = print $extraClass " sign-trusted" -}} - {{- else if eq $verification.TrustStatus "untrusted" -}} - {{- $extraClass = print $extraClass " sign-untrusted" -}} - {{- $msgReasonPrefix = ctx.Locale.Tr "repo.commits.signed_by_untrusted_user" -}} - {{- else -}} - {{- $extraClass = print $extraClass " sign-unmatched" -}} - {{- $msgReasonPrefix = ctx.Locale.Tr "repo.commits.signed_by_untrusted_user_unmatched" -}} - {{- end -}} - {{- else -}} - {{- if $verification.Warning -}} - {{- $extraClass = print $extraClass " sign-warning" -}} - {{- else -}} - {{- $extraClass = "" -}}{{/* the commit is not signed */}} - {{- end -}} - {{- $msgReason = ctx.Locale.Tr $verification.Reason -}}{{- /* dirty part: it is the translation key ..... */ -}} - {{- end -}} - - {{- if $msgReasonPrefix -}} - {{- $msgReason = print $msgReasonPrefix ": " $msgReason -}} - {{- end -}} - - {{- if $verification.SigningSSHKey -}} - {{- $msgSigningKey = print (ctx.Locale.Tr "repo.commits.ssh_key_fingerprint") ": " $verification.SigningSSHKey.Fingerprint -}} - {{- else if $verification.SigningKey -}}{{- /* asymkey.GPGKey */ -}} - {{- $msgSigningKey = print (ctx.Locale.Tr "repo.commits.gpg_key_id") ": " $verification.SigningKey.PaddedKeyID -}} - {{- end -}} -{{- end -}} - -{{- if $commit -}} - - {{- ShortSha $commit.ID.String -}} -{{- end -}} -{{- if or (not $commit) $extraClass}}{{/* only show the lock icon if there is no commit info (icon only) or the commit is really signed */}} - - {{- if $verified -}} - {{- if and $signingUser $signingUser.ID -}} - {{svg "gitea-lock"}} - {{ctx.AvatarUtils.Avatar $signingUser 20}} - {{- else -}} - {{svg "gitea-lock-cog"}} - {{ctx.AvatarUtils.AvatarByEmail $signingEmail "" 20}} - {{- end -}} - {{- else -}} - {{svg "gitea-unlock"}} - {{- end -}} - -{{- end -}} -{{- if $commit -}} - -{{- end -}} - -{{- /* This template should be kept as small as possible, DO NOT put large components like modal/dialog into it. */ -}} diff --git a/gitea/templates/repo/commits_list.tmpl b/gitea/templates/repo/commits_list.tmpl deleted file mode 100644 index 51a11160..00000000 --- a/gitea/templates/repo/commits_list.tmpl +++ /dev/null @@ -1,80 +0,0 @@ -
- - - - - - - - - - - - {{$commitRepoLink := $.RepoLink}}{{if $.CommitRepoLink}}{{$commitRepoLink = $.CommitRepoLink}}{{end}} - {{range $commit := $.Commits}} - {{$gitCommit := $commit.GitCommit}} - {{$commitID := $gitCommit.ID.String}} - - - - - - - - {{end}} - -
{{ctx.Locale.Tr "repo.commits.author"}}{{StringUtils.ToUpper $.Repository.ObjectFormatName}}{{ctx.Locale.Tr "repo.commits.message"}}{{ctx.Locale.Tr "repo.commits.date"}}
- {{ctx.RenderUtils.AvatarStackWithNames $commit.AvatarStackData}} - - {{$commitBaseLink := ""}} - {{if $.PageIsWiki}} - {{$commitBaseLink = printf "%s/wiki/commit" $commitRepoLink}} - {{else if $.PageIsPullCommits}} - {{$commitBaseLink = printf "%s/pulls/%d/commits" $commitRepoLink $.Issue.Index}} - {{else}} - {{$commitBaseLink = printf "%s/commit" $commitRepoLink}} - {{end}} - {{template "repo/commit_sign_badge" dict "Commit" $gitCommit "CommitBaseLink" $commitBaseLink "CommitSignVerification" .Verification}} - - - {{if $.PageIsWiki}} - - {{$gitCommit.MessageTitle | ctx.RenderUtils.RenderEmoji}} - - {{else}} - {{$commitLink:= printf "%s/commit/%s" $commitRepoLink (PathEscape $commitID)}} - - {{ctx.RenderUtils.RenderCommitMessageLinkSubject $gitCommit.MessageUTF8 $commitLink $.Repository}} - - {{end}} - - {{if $gitCommit.MessageBody}} - - {{end}} - {{template "repo/commit_statuses" dict "Status" .Status "Statuses" .Statuses}} - {{if $gitCommit.MessageBody}} -
{{ctx.RenderUtils.RenderCommitBody $gitCommit.MessageUTF8 $.Repository}}
- {{end}} - {{if $.CommitsTagsMap}} - {{range (index $.CommitsTagsMap $commitID)}} - {{- template "repo/tag/name" dict "AdditionalClasses" "tw-py-0" "RepoLink" $.Repository.Link "TagName" .TagName "IsRelease" (not .IsTag) -}} - {{end}} - {{end}} -
{{DateUtils.TimeSince $gitCommit.Committer.When}} - - {{/* at the moment, wiki doesn't support these "view" links like "view at history point" */}} - {{if not $.PageIsWiki}} - {{/* view single file diff */}} - {{if $.FileTreePath}} - {{svg "octicon-file-diff"}} - {{end}} - - {{/* view at history point */}} - {{$viewCommitLink := printf "%s/src/commit/%s" $commitRepoLink (PathEscape $commitID)}} - {{if $.FileTreePath}}{{$viewCommitLink = printf "%s/%s" $viewCommitLink (PathEscapeSegments $.FileTreePath)}}{{end}} - {{svg "octicon-file-code"}} - {{end}} -
-
diff --git a/gitea/templates/repo/view_content.tmpl b/gitea/templates/repo/view_content.tmpl deleted file mode 100644 index 77e051d7..00000000 --- a/gitea/templates/repo/view_content.tmpl +++ /dev/null @@ -1,136 +0,0 @@ -{{$isTreePathRoot := not .TreeNames}} - -
-{{template "repo/sub_menu" .}} -
-
- {{if not $isTreePathRoot}} - {{template "repo/view_file_tree_toggle_button" .}} - {{end}} - - {{template "repo/branch_dropdown" dict - "Repository" .Repository - "ShowTabBranches" true - "ShowTabTags" true - "CurrentRefType" .RefFullName.RefType - "CurrentRefShortName" .RefFullName.ShortName - "CurrentTreePath" .TreePath - "RefLinkTemplate" "{RepoLink}/src/{RefType}/{RefShortName}/{TreePath}" - "AllowCreateNewRef" .CanCreateBranch - "ShowViewAllRefsEntry" true - }} - - {{if and .PullRequestCtx.CanCreateNewPull .RefFullName.IsBranch}} - {{$compareLink := .PullRequestCtx.MakeDefaultCompareLink .BranchName}} - - {{svg "octicon-git-pull-request"}} - - {{end}} - - {{if and $isTreePathRoot .Repository.IsTemplate}} - - {{ctx.Locale.Tr "repo.use_template"}} - - {{end}} - - {{if not $isTreePathRoot}} - {{$treeNameIdxLast := Eval (len .TreeNames) "-" 1}} - - {{StringUtils.EllipsisString .Repository.Name 30}} - {{- range $i, $v := .TreeNames -}} - / - {{- if eq $i $treeNameIdxLast -}} - {{$v}} - - {{- else -}} - {{$p := index $.Paths $i}}{{$v}} - {{- end -}} - {{- end -}} - - {{end}} -
- -
-
T
- - {{if .RefFullName.IsBranch}} - {{$addFilePath := .TreePath}} - {{if .IsViewFile}} - {{if gt (len .TreeNames) 1}} - {{$addFilePath = StringUtils.Join (slice .TreeNames 0 (Eval (len .TreeNames) "-" 1)) "/"}} - {{else}} - {{$addFilePath = ""}} - {{end}} - {{end}} - - - {{if and (not .IsViewFile) (not $isTreePathRoot)}} - - {{end}} - {{end}} - - {{if $isTreePathRoot}} - {{template "repo/clone_panel" .}} - {{end}} - {{if and (not $isTreePathRoot) (not .IsViewFile) (not .IsBlame)}}{{/* IsViewDirectory (not home), TODO: split the templates, avoid using "if" tricks */}} - - {{svg "octicon-history"}}{{ctx.Locale.Tr "repo.file_history"}} - - {{end}} -
-
-{{if .IsViewFile}} - {{template "repo/view_file" .}} -{{else if .IsBlame}} - {{template "repo/blame" .}} -{{else}}{{/* IsViewDirectory */}} - {{if $isTreePathRoot}} - {{template "repo/code/upstream_diverging_info" .}} - {{end}} - {{template "repo/view_list" .}} - {{if and .ReadmeExist (or .RenderAsMarkup .IsPlainText)}} - {{template "repo/view_file" .}} - {{end}} -{{end}} diff --git a/gitea/templates/repo/view_list.tmpl b/gitea/templates/repo/view_list.tmpl deleted file mode 100644 index e79eac1d..00000000 --- a/gitea/templates/repo/view_list.tmpl +++ /dev/null @@ -1,67 +0,0 @@ -{{/* use grid layout, still use the old ID because there are many other CSS styles depending on this ID */}} -
-
- {{template "repo/latest_commit" .}} -
{{if and .LatestCommit .LatestCommit.Committer}}{{DateUtils.TimeSince .LatestCommit.Committer.When}}{{end}}
-
- {{$.FileIconPoolHTML}} - {{if .HasParentPath}} - - {{index $.FileIcons ".."}} .. - - {{end}} - {{range $item := .Files}} -
- {{$entry := $item.Entry}} - {{$commit := $item.Commit}} - {{$submoduleFile := $item.SubmoduleFile}} - -
- {{if $commit}} - {{$commitLink := printf "%s/commit/%s" $.RepoLink (PathEscape $commit.ID.String)}} - {{ctx.RenderUtils.RenderCommitMessageLinkSubject $commit.MessageUTF8 $commitLink $.Repository}} - {{else}} - … {{/* will be loaded again by LastCommitLoaderURL */}} - {{end}} -
-
{{if $commit}}{{DateUtils.TimeSince $commit.Committer.When}}{{end}}
-
- {{end}} -
diff --git a/gitea/web_src/css/themes/theme-gitea-auto-protanopia-deuteranopia.css b/gitea/web_src/css/themes/theme-gitea-auto-protanopia-deuteranopia.css deleted file mode 100644 index a5a08d9d..00000000 --- a/gitea/web_src/css/themes/theme-gitea-auto-protanopia-deuteranopia.css +++ /dev/null @@ -1,8 +0,0 @@ -@import "./theme-gitea-light-protanopia-deuteranopia.css" (prefers-color-scheme: light); -@import "./theme-gitea-dark-protanopia-deuteranopia.css" (prefers-color-scheme: dark); - -gitea-theme-meta-info { - --theme-display-name: "Auto"; - --theme-colorblind-type: "red-green"; - --theme-color-scheme: "auto"; -} diff --git a/gitea/web_src/css/themes/theme-gitea-auto-tritanopia.css b/gitea/web_src/css/themes/theme-gitea-auto-tritanopia.css deleted file mode 100644 index 178a2398..00000000 --- a/gitea/web_src/css/themes/theme-gitea-auto-tritanopia.css +++ /dev/null @@ -1,8 +0,0 @@ -@import "./theme-gitea-light-tritanopia.css" (prefers-color-scheme: light); -@import "./theme-gitea-dark-tritanopia.css" (prefers-color-scheme: dark); - -gitea-theme-meta-info { - --theme-display-name: "Auto"; - --theme-colorblind-type: "blue-yellow"; - --theme-color-scheme: "auto"; -} diff --git a/gitea/web_src/css/themes/theme-gitea-auto.css b/gitea/web_src/css/themes/theme-gitea-auto.css deleted file mode 100644 index 662ff4d6..00000000 --- a/gitea/web_src/css/themes/theme-gitea-auto.css +++ /dev/null @@ -1,7 +0,0 @@ -@import "./theme-gitea-light.css" (prefers-color-scheme: light); -@import "./theme-gitea-dark.css" (prefers-color-scheme: dark); - -gitea-theme-meta-info { - --theme-display-name: "Auto"; - --theme-color-scheme: "auto"; -} diff --git a/gitea/web_src/css/themes/theme-gitea-dark-protanopia-deuteranopia.css b/gitea/web_src/css/themes/theme-gitea-dark-protanopia-deuteranopia.css deleted file mode 100644 index 01f53e39..00000000 --- a/gitea/web_src/css/themes/theme-gitea-dark-protanopia-deuteranopia.css +++ /dev/null @@ -1,19 +0,0 @@ -@import "./theme-gitea-dark.css"; - -gitea-theme-meta-info { - --theme-display-name: "Dark"; - --theme-colorblind-type: "red-green"; - --theme-color-scheme: "dark"; -} - -/* red/green colorblind-friendly colors */ -:root { - --color-diff-added-fg: #58a6ff; - --color-diff-added-linenum-bg: #243d5d; - --color-diff-added-row-bg: #132339; - --color-diff-added-word-bg: #214d87; - --color-diff-removed-fg: #f0883e; - --color-diff-removed-linenum-bg: #5b361c; - --color-diff-removed-row-bg: #3c2419; - --color-diff-removed-word-bg: #824e1f; -} diff --git a/gitea/web_src/css/themes/theme-gitea-dark-tritanopia.css b/gitea/web_src/css/themes/theme-gitea-dark-tritanopia.css deleted file mode 100644 index dd4e0502..00000000 --- a/gitea/web_src/css/themes/theme-gitea-dark-tritanopia.css +++ /dev/null @@ -1,15 +0,0 @@ -@import "./theme-gitea-dark.css"; - -gitea-theme-meta-info { - --theme-display-name: "Dark"; - --theme-colorblind-type: "blue-yellow"; - --theme-color-scheme: "dark"; -} - -/* blue/yellow colorblind-friendly colors */ -:root { - --color-diff-added-fg: #58a6ff; - --color-diff-added-linenum-bg: #243d5d; - --color-diff-added-row-bg: #132339; - --color-diff-added-word-bg: #214d87; -} diff --git a/gitea/web_src/css/themes/theme-gitea-dark.css b/gitea/web_src/css/themes/theme-gitea-dark.css deleted file mode 100644 index f07d94cc..00000000 --- a/gitea/web_src/css/themes/theme-gitea-dark.css +++ /dev/null @@ -1,337 +0,0 @@ -gitea-theme-meta-info { - --theme-display-name: "Dark"; - --theme-color-scheme: "dark"; -} - -:root { - --is-dark-theme: true; - --color-primary: #4183c4; - --color-primary-contrast: #ffffff; - --color-primary-dark-1: #548fca; - --color-primary-dark-2: #679cd0; - --color-primary-dark-3: #7aa8d6; - --color-primary-dark-4: #8db5dc; - --color-primary-dark-5: #b3cde7; - --color-primary-dark-6: #d9e6f3; - --color-primary-dark-7: #f4f8fb; - --color-primary-light-1: #3876b3; - --color-primary-light-2: #31699f; - --color-primary-light-3: #2b5c8b; - --color-primary-light-4: #254f77; - --color-primary-light-5: #193450; - --color-primary-light-6: #0c1a28; - --color-primary-light-7: #04080c; - --color-primary-alpha-10: #4183c419; - --color-primary-alpha-20: #4183c433; - --color-primary-alpha-30: #4183c44b; - --color-primary-alpha-40: #4183c466; - --color-primary-alpha-50: #4183c480; - --color-primary-alpha-60: #4183c499; - --color-primary-alpha-70: #4183c4b3; - --color-primary-alpha-80: #4183c4cc; - --color-primary-alpha-90: #4183c4e1; - --color-primary-hover: var(--color-primary-light-1); - --color-primary-active: var(--color-primary-light-2); - --color-secondary: #3f4248; - --color-secondary-dark-1: #46494f; - --color-secondary-dark-2: #4f5259; - --color-secondary-dark-3: #5e626a; - --color-secondary-dark-4: #6f747d; - --color-secondary-dark-5: #7d828c; - --color-secondary-dark-6: #8b8f98; - --color-secondary-dark-7: #999da4; - --color-secondary-dark-8: #a8abb1; - --color-secondary-dark-9: #aeb1b8; - --color-secondary-dark-10: #bbbec3; - --color-secondary-dark-11: #c8cacf; - --color-secondary-dark-12: #d2d4d7; - --color-secondary-dark-13: #d5d6d9; - --color-secondary-light-1: #35373c; - --color-secondary-light-2: #2c2e32; - --color-secondary-light-3: #1f2124; - --color-secondary-light-4: #191a1c; - --color-secondary-alpha-10: #3f424819; - --color-secondary-alpha-20: #3f424833; - --color-secondary-alpha-30: #3f42484b; - --color-secondary-alpha-40: #3f424866; - --color-secondary-alpha-50: #3f424880; - --color-secondary-alpha-60: #3f424899; - --color-secondary-alpha-70: #3f4248b3; - --color-secondary-alpha-80: #3f4248cc; - --color-secondary-alpha-90: #3f4248e1; - --color-secondary-button: var(--color-secondary-dark-4); - --color-secondary-hover: var(--color-secondary-dark-3); - --color-secondary-active: var(--color-secondary-dark-2); - /* console colors - used for actions console and console files */ - --color-console-fg: #f8f8f8; - --color-console-fg-subtle: #c1c3c8; - --color-console-bg: #191a1c; - --color-console-border: #313338; - --color-console-hover-bg: #2a2c30; - --color-console-active-bg: #313338; - --color-console-menu-bg: #292b2e; - --color-console-menu-border: #46494f; - --color-console-link: #969aa1; - /* named colors */ - --color-red: #cc4848; - --color-orange: #cc580c; - --color-yellow: #cc9903; - --color-olive: #91a313; - --color-green: #87ab63; - --color-teal: #00918a; - --color-blue: #3a8ac6; - --color-violet: #906ae1; - --color-purple: #b259d0; - --color-pink: #d22e8b; - --color-brown: #a47252; - --color-black: #202225; - /* light variants - produced via Sass scale-color(color, $lightness: +10%) */ - --color-red-light: #d15a5a; - --color-orange-light: #f6a066; - --color-yellow-light: #eaaf03; - --color-olive-light: #abc016; - --color-green-light: #93b373; - --color-teal-light: #00b6ad; - --color-blue-light: #4e96cc; - --color-violet-light: #9b79e4; - --color-purple-light: #ba6ad5; - --color-pink-light: #d74397; - --color-brown-light: #b08061; - --color-black-light: #45484e; - /* dark 1 variants - produced via Sass scale-color(color, $lightness: -10%) */ - --color-red-dark-1: #c23636; - --color-orange-dark-1: #f38236; - --color-yellow-dark-1: #b88a03; - --color-olive-dark-1: #839311; - --color-green-dark-1: #7a9e55; - --color-teal-dark-1: #00837c; - --color-blue-dark-1: #347cb3; - --color-violet-dark-1: #7b4edb; - --color-purple-dark-1: #a742c9; - --color-pink-dark-1: #be297d; - --color-brown-dark-1: #94674a; - --color-black-dark-1: #2e3033; - /* dark 2 variants - produced via Sass scale-color(color, $lightness: -20%) */ - --color-red-dark-2: #ad3030; - --color-orange-dark-2: #f16e17; - --color-yellow-dark-2: #a37a02; - --color-olive-dark-2: #74820f; - --color-green-dark-2: #6c8c4c; - --color-teal-dark-2: #00746e; - --color-blue-dark-2: #2e6e9f; - --color-violet-dark-2: #6733d6; - --color-purple-dark-2: #9834b9; - --color-pink-dark-2: #a9246f; - --color-brown-dark-2: #835b42; - --color-black-dark-2: #292b2e; - /* ansi colors used for actions console and console files */ - --color-ansi-black: #202225; - --color-ansi-red: #cc4848; - --color-ansi-green: #87ab63; - --color-ansi-yellow: #cc9903; - --color-ansi-blue: #3a8ac6; - --color-ansi-magenta: #d22e8b; - --color-ansi-cyan: #00918a; - --color-ansi-white: var(--color-console-fg-subtle); - --color-ansi-bright-black: #45484e; - --color-ansi-bright-red: #d15a5a; - --color-ansi-bright-green: #93b373; - --color-ansi-bright-yellow: #eaaf03; - --color-ansi-bright-blue: #4e96cc; - --color-ansi-bright-magenta: #d74397; - --color-ansi-bright-cyan: #00b6ad; - --color-ansi-bright-white: var(--color-console-fg); - /* 16-color series */ - --color-series-16-0: #7db233; - --color-series-16-1: #499a37; - --color-series-16-2: #ce4751; - --color-series-16-3: #8f9121; - --color-series-16-4: #ac32a6; - --color-series-16-5: #7445e9; - --color-series-16-6: #c67d28; - --color-series-16-7: #4db392; - --color-series-16-8: #aa4d30; - --color-series-16-9: #2a6f84; - --color-series-16-10: #c45327; - --color-series-16-11: #3d965c; - --color-series-16-12: #792a93; - --color-series-16-13: #439d73; - --color-series-16-14: #103aad; - --color-series-16-15: #982e85; - /* other colors */ - --color-grey: #3d3f44; - --color-grey-light: #898d96; - --color-gold: #b1983b; - --color-white: #ffffff; - --color-diff-added-fg: #87ab63; - --color-diff-added-linenum-bg: #274227; - --color-diff-added-row-bg: #203224; - --color-diff-added-row-border: #314a37; - --color-diff-added-word-bg: #3c653c; - --color-diff-moved-row-bg: #818044; - --color-diff-moved-row-border: #bcca6f; - --color-diff-removed-fg: #cc4848; - --color-diff-removed-linenum-bg: #482121; - --color-diff-removed-row-bg: #301e1e; - --color-diff-removed-row-border: #634343; - --color-diff-removed-word-bg: #6f3333; - --color-diff-inactive: #25272a; - --color-error-border: #763232; - --color-error-bg: #322226; - --color-error-bg-active: #49262a; - --color-error-bg-hover: #3c2427; - --color-error-text: #f85149; - --color-success-border: #225633; - --color-success-bg: #1c3329; - --color-success-text: #3fb950; - --color-warning-border: #5f481a; - --color-warning-bg: #342e1f; - --color-warning-text: #d29922; - --color-info-border: #254a7e; - --color-info-bg: #1b283a; - --color-info-text: #2f81f7; - --color-priority-border: #4a268d; - --color-priority-bg: #251c39; - --color-priority-text: #a371f7; - --color-red-badge: #db2828; - --color-red-badge-bg: #db28281a; - --color-red-badge-hover-bg: #db28284d; - --color-green-badge: #21ba45; - --color-green-badge-bg: #21ba451a; - --color-green-badge-hover-bg: #21ba454d; - --color-yellow-badge: #fbbd08; - --color-yellow-badge-bg: #fbbd081a; - --color-yellow-badge-hover-bg: #fbbd084d; - --color-orange-badge: #f2711c; - --color-orange-badge-bg: #f2711c1a; - --color-orange-badge-hover-bg: #f2711c4d; - --color-git: #f05133; - --color-logo: #609926; - /* target-based colors */ - --color-body: #1e1f20; - --color-box-header: #1b1c1e; - --color-box-body: #161718; - --color-box-body-highlight: #202124; - --color-text-dark: #f8f8f8; - --color-text: #d2d4d8; - --color-text-light: #c0c2c7; - --color-text-light-1: #aaadb4; - --color-text-light-2: #969aa1; - --color-text-light-3: #80858f; - --color-footer: var(--color-nav-bg); - --color-timeline: #383b40; - --color-input-text: var(--color-text-dark); - --color-input-background: #191a1c; - --color-input-toggle-background: #323438; - --color-input-border: var(--color-secondary-dark-1); - --color-light: #0b0b0c28; - --color-light-border: #f3f3f428; - --color-hover: #f3f3f419; - --color-hover-opaque: #232528; /* TODO: color-mix(in srgb, var(--color-body), var(--color-hover)); */ - --color-active: #f3f3f424; - --color-menu: #191a1c; - --color-card: #191a1c; - --color-markup-table-row: #f3f3f40f; - --color-markup-code-block: #f3f3f412; - --color-markup-code-inline: #f3f3f428; - --color-button: #191a1c; - --color-code-bg: #161718; - --color-shadow: #0b0b0c58; - --color-shadow-opaque: #0b0b0c; - --color-secondary-bg: #2e3033; - --color-expand-button: #333539; - --color-placeholder-text: var(--color-text-light-3); - --color-editor-line-highlight: var(--color-secondary-alpha-40); - --color-editor-selection: var(--color-primary-alpha-50); - --color-project-column-bg: var(--color-secondary-light-2); - --color-caret: var(--color-text); /* should ideally be --color-text-dark, see #15651 */ - --color-reaction-bg: #f3f3f412; - --color-reaction-hover-bg: var(--color-primary-light-4); - --color-reaction-active-bg: var(--color-primary-light-5); - --color-tooltip-text: #fafafa; - --color-tooltip-bg: #0b0b0cf0; - --color-nav-bg: #18191b; - --color-nav-hover-bg: var(--color-secondary-light-1); - --color-nav-text: var(--color-text); - --color-secondary-nav-bg: #1a1b1e; - --color-label-text: var(--color-text); - --color-label-bg: #7a7f8a4b; - --color-label-hover-bg: #7a7f8aa0; - --color-label-active-bg: #7a7f8aff; - --color-accent: var(--color-primary-light-1); - --color-small-accent: var(--color-primary-light-5); - --color-highlight-fg: #87651e; - --color-highlight-bg: #443a27; - --color-overlay-backdrop: #080808c0; - --color-danger: var(--color-red); - --color-transparency-grid-light: #2a2a2a; - --color-transparency-grid-dark: #1a1a1a; - --color-workflow-edge-hover: #666a73; - --color-syntax-keyword: #ff8854; - --color-syntax-bool: #25bbc9; - --color-syntax-control: #dd9e17; - --color-syntax-name: #c7a618; - --color-syntax-type: #eb8cb3; - --color-syntax-number: #63b2dd; - --color-syntax-operator: #ff8854; - --color-syntax-regexp: #b89de4; - --color-syntax-string: #95b62a; - --color-syntax-comment: #8898b0; - --color-syntax-invalid: #ff8686; - --color-syntax-link: var(--color-primary); - --color-syntax-tag: #ff8854; - --color-syntax-attribute: #c792ff; - --color-syntax-property: #55afff; - --color-syntax-variable: #e29b33; - --color-syntax-string-special: #dd9e17; - --color-syntax-escape: #c7a618; - --color-syntax-entity: #c792ff; - --color-syntax-preproc: #4cbe7a; - --color-syntax-preproc-file: #63b2dd; - --color-syntax-decorator: #4cbe7a; - --color-syntax-namespace: #c9d1d9; - --color-syntax-name-pseudo: #c792ff; - --color-syntax-comment-special: #b89de4; - --color-syntax-text: #c9d1d9; - --color-syntax-text-alt: #b9bcc7; - --color-syntax-punctuation: #d2d4db; - --color-syntax-whitespace: #7f8699; - --color-syntax-diff-fg: #ffffff; - --color-syntax-deleted-bg: #5f3737; - --color-syntax-inserted-bg: #3a523a; - --color-syntax-emph: #d1a242; - --color-syntax-strong: #e29b33; - --color-syntax-heading: #dd9e17; - --color-syntax-subheading: #95b62a; - --color-syntax-output: #8898b0; - --color-syntax-prompt: #e29b33; - --color-syntax-traceback: #ff8686; - --color-syntax-matching-bracket-bg: #00918a48; - --color-syntax-nonmatching-bracket-bg: #cc484848; - accent-color: var(--color-accent); - color-scheme: dark; -} - -/* invert emojis that are hard to read otherwise */ -.emoji[aria-label="check mark"], -.emoji[aria-label="currency exchange"], -.emoji[aria-label="TOP arrow"], -.emoji[aria-label="END arrow"], -.emoji[aria-label="ON! arrow"], -.emoji[aria-label="SOON arrow"], -.emoji[aria-label="heavy dollar sign"], -.emoji[aria-label="copyright"], -.emoji[aria-label="registered"], -.emoji[aria-label="trade mark"], -.emoji[aria-label="multiply"], -.emoji[aria-label="plus"], -.emoji[aria-label="minus"], -.emoji[aria-label="divide"], -.emoji[aria-label="curly loop"], -.emoji[aria-label="double curly loop"], -.emoji[aria-label="wavy dash"], -.emoji[aria-label="paw prints"], -.emoji[aria-label="musical note"], -.emoji[aria-label="musical notes"] { - filter: invert(100%) hue-rotate(180deg); -} diff --git a/gitea/web_src/css/themes/theme-gitea-light-protanopia-deuteranopia.css b/gitea/web_src/css/themes/theme-gitea-light-protanopia-deuteranopia.css deleted file mode 100644 index 6350cdd1..00000000 --- a/gitea/web_src/css/themes/theme-gitea-light-protanopia-deuteranopia.css +++ /dev/null @@ -1,19 +0,0 @@ -@import "./theme-gitea-light.css"; - -gitea-theme-meta-info { - --theme-display-name: "Light"; - --theme-colorblind-type: "red-green"; - --theme-color-scheme: "light"; -} - -/* red/green colorblind-friendly colors */ -:root { - --color-diff-added-fg: #2185d0; - --color-diff-added-linenum-bg: #b6e3ff; - --color-diff-added-row-bg: #ddf4ff; - --color-diff-added-word-bg: #b6e3ff; - --color-diff-removed-fg: #fc6500; - --color-diff-removed-linenum-bg: #ffd8b5; - --color-diff-removed-row-bg: #fff1e5; - --color-diff-removed-word-bg: #ffd8b5; -} diff --git a/gitea/web_src/css/themes/theme-gitea-light-tritanopia.css b/gitea/web_src/css/themes/theme-gitea-light-tritanopia.css deleted file mode 100644 index 15f3019b..00000000 --- a/gitea/web_src/css/themes/theme-gitea-light-tritanopia.css +++ /dev/null @@ -1,15 +0,0 @@ -@import "./theme-gitea-light.css"; - -gitea-theme-meta-info { - --theme-display-name: "Light"; - --theme-colorblind-type: "blue-yellow"; - --theme-color-scheme: "light"; -} - -/* blue/yellow colorblind-friendly colors */ -:root { - --color-diff-added-fg: #2185d0; - --color-diff-added-linenum-bg: #b6e3ff; - --color-diff-added-row-bg: #ddf4ff; - --color-diff-added-word-bg: #b6e3ff; -} diff --git a/gitea/web_src/css/themes/theme-gitea-light.css b/gitea/web_src/css/themes/theme-gitea-light.css deleted file mode 100644 index 1bd7dd08..00000000 --- a/gitea/web_src/css/themes/theme-gitea-light.css +++ /dev/null @@ -1,313 +0,0 @@ -gitea-theme-meta-info { - --theme-display-name: "Light"; - --theme-color-scheme: "light"; -} - -:root { - --is-dark-theme: false; - --color-primary: #4183c4; - --color-primary-contrast: #ffffff; - --color-primary-dark-1: #3876b3; - --color-primary-dark-2: #31699f; - --color-primary-dark-3: #2b5c8b; - --color-primary-dark-4: #254f77; - --color-primary-dark-5: #193450; - --color-primary-dark-6: #0c1a28; - --color-primary-dark-7: #04080c; - --color-primary-light-1: #548fca; - --color-primary-light-2: #679cd0; - --color-primary-light-3: #7aa8d6; - --color-primary-light-4: #8db5dc; - --color-primary-light-5: #b3cde7; - --color-primary-light-6: #d9e6f3; - --color-primary-light-7: #f4f8fb; - --color-primary-alpha-10: #4183c419; - --color-primary-alpha-20: #4183c433; - --color-primary-alpha-30: #4183c44b; - --color-primary-alpha-40: #4183c466; - --color-primary-alpha-50: #4183c480; - --color-primary-alpha-60: #4183c499; - --color-primary-alpha-70: #4183c4b3; - --color-primary-alpha-80: #4183c4cc; - --color-primary-alpha-90: #4183c4e1; - --color-primary-hover: var(--color-primary-dark-1); - --color-primary-active: var(--color-primary-dark-2); - --color-secondary: #d0d7de; - --color-secondary-dark-1: #c7ced5; - --color-secondary-dark-2: #b9c0c7; - --color-secondary-dark-3: #99a0a7; - --color-secondary-dark-4: #899097; - --color-secondary-dark-5: #7a8188; - --color-secondary-dark-6: #6a7178; - --color-secondary-dark-7: #5b6269; - --color-secondary-dark-8: #4b5259; - --color-secondary-dark-9: #3c434a; - --color-secondary-dark-10: #2c333a; - --color-secondary-dark-11: #1d242b; - --color-secondary-dark-12: #0d141b; - --color-secondary-dark-13: #00040b; - --color-secondary-light-1: #dee5ec; - --color-secondary-light-2: #e4ebf2; - --color-secondary-light-3: #ebf2f9; - --color-secondary-light-4: #f1f8ff; - --color-secondary-alpha-10: #d0d7de19; - --color-secondary-alpha-20: #d0d7de33; - --color-secondary-alpha-30: #d0d7de4b; - --color-secondary-alpha-40: #d0d7de66; - --color-secondary-alpha-50: #d0d7de80; - --color-secondary-alpha-60: #d0d7de99; - --color-secondary-alpha-70: #d0d7deb3; - --color-secondary-alpha-80: #d0d7decc; - --color-secondary-alpha-90: #d0d7dee1; - --color-secondary-button: var(--color-secondary-dark-4); - --color-secondary-hover: var(--color-secondary-dark-5); - --color-secondary-active: var(--color-secondary-dark-6); - /* console colors - used for actions console and console files */ - --color-console-fg: #0d1117; - --color-console-fg-subtle: #40474d; - --color-console-bg: #ffffff; - --color-console-border: #d0d7de; - --color-console-hover-bg: #f1f3f5; - --color-console-active-bg: #d0d7de; - --color-console-menu-bg: #f8f9fb; - --color-console-menu-border: #d0d7de; - --color-console-link: #5c656d; - /* named colors */ - --color-red: #db2828; - --color-orange: #f2711c; - --color-yellow: #fbbd08; - --color-olive: #b5cc18; - --color-green: #21ba45; - --color-teal: #00b5ad; - --color-blue: #2185d0; - --color-violet: #6435c9; - --color-purple: #a333c8; - --color-pink: #e03997; - --color-brown: #a5673f; - --color-black: #1d2328; - /* light variants - produced via Sass scale-color(color, $lightness: +25%) */ - --color-red-light: #e45e5e; - --color-orange-light: #f59555; - --color-yellow-light: #fcce46; - --color-olive-light: #d3e942; - --color-green-light: #46de6a; - --color-teal-light: #08fff4; - --color-blue-light: #51a5e3; - --color-violet-light: #8b67d7; - --color-purple-light: #bb64d8; - --color-pink-light: #e86bb1; - --color-brown-light: #c58b66; - --color-black-light: #4b5b68; - /* dark 1 variants - produced via Sass scale-color(color, $lightness: -10%) */ - --color-red-dark-1: #c82121; - --color-orange-dark-1: #e6630d; - --color-yellow-dark-1: #e5ac04; - --color-olive-dark-1: #a3b816; - --color-green-dark-1: #1ea73e; - --color-teal-dark-1: #00a39c; - --color-blue-dark-1: #1e78bb; - --color-violet-dark-1: #5a30b5; - --color-purple-dark-1: #932eb4; - --color-pink-dark-1: #db228a; - --color-brown-dark-1: #955d39; - --color-black-dark-1: #2c3339; - /* dark 2 variants - produced via Sass scale-color(color, $lightness: -20%) */ - --color-red-dark-2: #b11e1e; - --color-orange-dark-2: #cc580c; - --color-yellow-dark-2: #cc9903; - --color-olive-dark-2: #91a313; - --color-green-dark-2: #1a9537; - --color-teal-dark-2: #00918a; - --color-blue-dark-2: #1a6aa6; - --color-violet-dark-2: #502aa1; - --color-purple-dark-2: #8229a0; - --color-pink-dark-2: #c21e7b; - --color-brown-dark-2: #845232; - --color-black-dark-2: #131619; - /* ansi colors used for actions console and console files */ - --color-ansi-black: #1e2327; - --color-ansi-red: #cc4848; - --color-ansi-green: #87ab63; - --color-ansi-yellow: #cc9903; - --color-ansi-blue: #3a8ac6; - --color-ansi-magenta: #d22e8b; - --color-ansi-cyan: #00918a; - --color-ansi-white: var(--color-console-fg-subtle); - --color-ansi-bright-black: #46494d; - --color-ansi-bright-red: #d15a5a; - --color-ansi-bright-green: #93b373; - --color-ansi-bright-yellow: #eaaf03; - --color-ansi-bright-blue: #4e96cc; - --color-ansi-bright-magenta: #d74397; - --color-ansi-bright-cyan: #00b6ad; - --color-ansi-bright-white: var(--color-console-fg); - /* 16-color series */ - --color-series-16-0: #7db233; - --color-series-16-1: #499a37; - --color-series-16-2: #ce4751; - --color-series-16-3: #8f9121; - --color-series-16-4: #ac32a6; - --color-series-16-5: #7445e9; - --color-series-16-6: #c67d28; - --color-series-16-7: #4db392; - --color-series-16-8: #aa4d30; - --color-series-16-9: #2a6f84; - --color-series-16-10: #c45327; - --color-series-16-11: #3d965c; - --color-series-16-12: #792a93; - --color-series-16-13: #439d73; - --color-series-16-14: #103aad; - --color-series-16-15: #982e85; - /* other colors */ - --color-grey: #697077; - --color-grey-light: #7c838a; - --color-gold: #a1882b; - --color-white: #ffffff; - --color-diff-added-fg: #21ba45; - --color-diff-added-linenum-bg: #d1f8d9; - --color-diff-added-row-bg: #e6ffed; - --color-diff-added-row-border: #e6ffed; - --color-diff-added-word-bg: #acf2bd; - --color-diff-moved-row-bg: #f1f8d1; - --color-diff-moved-row-border: #d0e27f; - --color-diff-removed-fg: #db2828; - --color-diff-removed-linenum-bg: #ffcecb; - --color-diff-removed-row-bg: #ffeef0; - --color-diff-removed-row-border: #f1c0c0; - --color-diff-removed-word-bg: #fdb8c0; - --color-diff-inactive: #f0f2f4; - --color-error-border: #ff818266; - --color-error-bg: #ffebe9; - --color-error-bg-active: #ffcecb; - --color-error-bg-hover: #ffdcd7; - --color-error-text: #d1242f; - --color-success-border: #4ac26b66; - --color-success-bg: #dafbe1; - --color-success-text: #1a7f37; - --color-warning-border: #d4a72c66; - --color-warning-bg: #fff8c5; - --color-warning-text: #9a6700; - --color-info-border: #54aeff66; - --color-info-bg: #ddf4ff; - --color-info-text: #0969da; - --color-priority-border: #b9a1ff66; - --color-priority-bg: #f3e7ff; - --color-priority-text: #8250df; - --color-red-badge: #db2828; - --color-red-badge-bg: #db28281a; - --color-red-badge-hover-bg: #db28284d; - --color-green-badge: #21ba45; - --color-green-badge-bg: #21ba451a; - --color-green-badge-hover-bg: #21ba454d; - --color-yellow-badge: #fbbd08; - --color-yellow-badge-bg: #fbbd081a; - --color-yellow-badge-hover-bg: #fbbd084d; - --color-orange-badge: #f2711c; - --color-orange-badge-bg: #f2711c1a; - --color-orange-badge-hover-bg: #f2711c4d; - --color-git: #f05133; - --color-logo: #609926; - /* target-based colors */ - --color-body: #ffffff; - --color-box-header: #f1f3f5; - --color-box-body: #ffffff; - --color-box-body-highlight: #ecf5fd; - --color-text-dark: #01050a; - --color-text: #181c21; - --color-text-light: #30363b; - --color-text-light-1: #40474d; - --color-text-light-2: #5b6167; - --color-text-light-3: #747c84; - --color-footer: var(--color-nav-bg); - --color-timeline: #d0d7de; - --color-input-text: var(--color-text-dark); - --color-input-background: #fff; - --color-input-toggle-background: #d0d7de; - --color-input-border: var(--color-secondary-dark-1); - --color-light: #00001706; - --color-light-border: #0000171d; - --color-hover: #00001708; - --color-hover-opaque: #f1f3f5; /* TODO: color-mix(in srgb, var(--color-body), var(--color-hover)); */ - --color-active: #00001714; - --color-menu: #f8f9fb; - --color-card: #f8f9fb; - --color-markup-table-row: #0030600a; - --color-markup-code-block: #00306010; - --color-markup-code-inline: #00306012; - --color-button: #f8f9fb; - --color-code-bg: #fafdff; - --color-shadow: #00001726; - --color-shadow-opaque: #c7ced5; - --color-secondary-bg: #f2f5f8; - --color-expand-button: #cfe8fa; - --color-placeholder-text: var(--color-text-light-3); - --color-editor-line-highlight: var(--color-secondary-alpha-30); - --color-editor-selection: var(--color-primary-alpha-30); - --color-project-column-bg: var(--color-secondary-light-4); - --color-caret: var(--color-text-dark); - --color-reaction-bg: #0000170a; - --color-reaction-hover-bg: var(--color-primary-light-5); - --color-reaction-active-bg: var(--color-primary-light-6); - --color-tooltip-text: #fbfdff; - --color-tooltip-bg: #000017f0; - --color-nav-bg: #f6f7fa; - --color-nav-hover-bg: var(--color-secondary-light-1); - --color-nav-text: var(--color-text); - --color-secondary-nav-bg: #f9fafb; - --color-label-text: var(--color-text); - --color-label-bg: #949da64b; - --color-label-hover-bg: #949da6a0; - --color-label-active-bg: #949da6ff; - --color-accent: var(--color-primary-light-1); - --color-small-accent: var(--color-primary-light-6); - --color-highlight-fg: #eed200; - --color-highlight-bg: #f5efc5; - --color-overlay-backdrop: #080808c0; - --color-danger: var(--color-red); - --color-transparency-grid-light: #fafafa; - --color-transparency-grid-dark: #e2e2e2; - --color-workflow-edge-hover: #b1b7bd; - --color-syntax-keyword: #a73a00; - --color-syntax-bool: #076872; - --color-syntax-control: #7d5700; - --color-syntax-name: #785900; - --color-syntax-type: #ae2368; - --color-syntax-number: #105ead; - --color-syntax-operator: #a73a00; - --color-syntax-regexp: #773dc5; - --color-syntax-string: #456800; - --color-syntax-comment: #506070; - --color-syntax-invalid: #c00000; - --color-syntax-link: var(--color-primary); - --color-syntax-tag: #a73a00; - --color-syntax-attribute: #6f41c5; - --color-syntax-property: #2060a0; - --color-syntax-variable: #944a00; - --color-syntax-string-special: #7d5700; - --color-syntax-escape: #785900; - --color-syntax-entity: #6f41c5; - --color-syntax-preproc: #2d6a4b; - --color-syntax-preproc-file: #105ead; - --color-syntax-decorator: #2d6a4b; - --color-syntax-namespace: #555555; - --color-syntax-name-pseudo: #6f41c5; - --color-syntax-comment-special: #773dc5; - --color-syntax-text: inherit; - --color-syntax-text-alt: #47525b; - --color-syntax-punctuation: inherit; - --color-syntax-whitespace: #bbbbbb; - --color-syntax-diff-fg: #000000; - --color-syntax-deleted-bg: #ffdddd; - --color-syntax-inserted-bg: #ddffdd; - --color-syntax-emph: #8b5000; - --color-syntax-strong: inherit; - --color-syntax-heading: #7d5700; - --color-syntax-subheading: #456800; - --color-syntax-output: #506070; - --color-syntax-prompt: #944a00; - --color-syntax-traceback: #c00000; - --color-syntax-matching-bracket-bg: #00b5ad38; - --color-syntax-nonmatching-bracket-bg: #db282838; - accent-color: var(--color-accent); - color-scheme: light; -} diff --git a/package.json b/package.json index 80dfc27f..1b3a7b12 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,6 @@ "bundle:ci": "bun bundle && bun vitest", "sync:theme": "bun scripts/syncTheme.ts", "update:primer": "bun scripts/buildPrimerTSToken.ts", - "update:gitea": "bun scripts/updateGitea.ts", "update:snapshot": "bun vitest --update", "install:clean": "rm -rf node_modules bun.lock bun.lockb && bun install" }, @@ -57,7 +56,6 @@ "style-dictionary": "^5.5.0", "typescript": "^7.0.2", "typescript-eslint": "^8.63.0", - "undici": "^7.28.0", "vite": "^8.1.4", "vitest": "4.1.10" }, diff --git a/scripts/assembleForgejo.ts b/scripts/assembleForgejo.ts index ae3d6482..7c67543c 100644 --- a/scripts/assembleForgejo.ts +++ b/scripts/assembleForgejo.ts @@ -43,25 +43,8 @@ for (const directory of readdirSync(join(root, "components"))) { } } -for (const file of [ - "blame.css", - "commits.css", - "create.css", - "dashboard-home.css", - "dashboard-lists.css", - "find-file.css", - "insights.css", - "organization.css", - "profile-subpages.css", - "projects.css", - "pull-request.css", - "releases.css", - "settings.css", - "site.css", - "user-profile.css", - "wiki.css", -]) { - cpSync(join(root, "styles", "pages", file), join(css, "components", "pages", file)); +for (const file of readdirSync(join(root, "styles", "pages"))) { + if (file.endsWith(".css")) cpSync(join(root, "styles", "pages", file), join(css, "components", "pages", file)); } for (const file of readdirSync(join(root, "public"))) { if (file.endsWith(".css")) cpSync(join(root, "public", file), join(css, file)); @@ -89,9 +72,7 @@ for (const file of themeFiles) { ); if (stamped !== source) writeFileSync(join(dist, file), stamped); } -for (const file of readdirSync(dist)) { - if (file.endsWith(".css")) cpSync(join(dist, file), join(css, file)); -} +for (const file of themeFiles) cpSync(join(dist, file), join(css, file)); // Forgejo links the selected theme with its own release as the only version. The theme's head_style override // gets the version of all theme files appended, so an updated theme is fetched instead of served from cache. diff --git a/scripts/updateGitea.ts b/scripts/updateGitea.ts deleted file mode 100644 index ce2aab2f..00000000 --- a/scripts/updateGitea.ts +++ /dev/null @@ -1,71 +0,0 @@ -/*! - * Copyright (c) https://github.com/lutinglt - * - * See the NOTICE file distributed with this work for additional - * information regarding copyright ownership. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import fs from "node:fs"; -import path from "node:path"; -import { pkg, rootDir, setGlobalProxy } from "./utils"; - -setGlobalProxy(); - -const githubSite = "https://raw.githubusercontent.com"; -const giteaRepo = "go-gitea/gitea"; -const githubTagPath = "refs/tags"; -const localGiteaPath = path.join(rootDir, "gitea"); -const [major, minor, patch = ""] = pkg.version.split("."); -console.log("Version:", pkg.version); -const versionPath = `${githubTagPath}/v${major}.${minor}.${patch}`; -const githubBaseUrl = `${githubSite}/${giteaRepo}/${versionPath}`; - -// 递归读取所有子目录中的文件 -function readFilesRecursively(dir: string) { - const results: string[] = []; - const files = fs.readdirSync(dir); - - for (const file of files) { - const filePath = path.join(dir, file); - const stat = fs.statSync(filePath); - - if (stat.isDirectory()) { - // 如果是目录,递归读取 - results.push(...readFilesRecursively(filePath)); - } else { - // 添加到结果中(相对于 gitea 目录的相对路径) - const relativePath = path.relative(localGiteaPath, filePath); - results.push(relativePath); - } - } - - return results; -} - -// 读取 gitea 目录下所有文件 -const files = readFilesRecursively(localGiteaPath); -(async () => { - for (const file of files) { - const fileUrl = `${githubBaseUrl}/${file}`; - - const response = await fetch(fileUrl); - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); - } - const content = await response.text(); - fs.writeFileSync(path.join(localGiteaPath, file), content); - console.log("✔︎ FileUrl:", fileUrl); - } -})(); diff --git a/scripts/utils.ts b/scripts/utils.ts index 977b0418..0178824f 100644 --- a/scripts/utils.ts +++ b/scripts/utils.ts @@ -17,18 +17,6 @@ * limitations under the License. */ -import path from "node:path"; -import { fileURLToPath } from "node:url"; -import { ProxyAgent, setGlobalDispatcher } from "undici"; import pkg from "../package.json"; -export function setGlobalProxy() { - const proxyUrl = process.env.HTTPS_PROXY || process.env.HTTP_PROXY || process.env.ALL_PROXY; - if (proxyUrl) { - setGlobalDispatcher(new ProxyAgent(proxyUrl)); - } -} - export { pkg }; -export const __dirname = path.dirname(fileURLToPath(import.meta.url)); -export const rootDir = path.join(__dirname, ".."); diff --git a/src/build-output.test.ts b/src/build-output.test.ts index 1ae28756..a946d92b 100644 --- a/src/build-output.test.ts +++ b/src/build-output.test.ts @@ -110,8 +110,7 @@ describe("构建产物基本验证", () => { it("无多余主题文件", () => { const actualFiles = fs.readdirSync(DIST_DIR).filter(f => f.endsWith(".css")); const expectedSet = new Set(EXPECTED_THEME_FILES); - const pageStyles = new Set(fs.readdirSync(path.join(ROOT_DIR, "public")).filter(f => f.endsWith(".css"))); - const extra = actualFiles.filter(f => !expectedSet.has(f) && !pageStyles.has(f)); + const extra = actualFiles.filter(f => !expectedSet.has(f)); expect(extra, `存在多余主题文件: ${extra.join(", ")}`).toEqual([]); }); }); @@ -123,33 +122,43 @@ it("ships component stylesheets unchanged with valid CSS and a template referenc .filter(file => String(file).endsWith(".tmpl")) .map(file => fs.readFileSync(path.join(ROOT_DIR, "templates", String(file)), "utf-8")) .join("\n"); + // [source in the repository, path under css/ in the Forgejo tree] const sources = [ ...fs .readdirSync(path.join(ROOT_DIR, "public")) .filter(name => name.endsWith(".css")) - .map(name => [name, name]), + .map(name => [`public/${name}`, name]), ...fs .readdirSync(path.join(ROOT_DIR, "components"), { recursive: true }) .filter(file => String(file).endsWith(".css")) .map(file => [`components/${String(file)}`, `components/${String(file)}`]), - ...["blame.css", "find-file.css", "organization.css", "profile-subpages.css", "user-profile.css", "wiki.css"].map( - name => [name, `components/pages/${name}`] - ), + ...fs + .readdirSync(path.join(ROOT_DIR, "styles", "pages")) + .filter(name => name.endsWith(".css")) + .map(name => [`styles/pages/${name}`, `components/pages/${name}`]), ]; const output = path.join(DIST_DIR, "forgejo", "public", "assets", "css"); for (const [source, asset] of sources) { const code = fs.readFileSync(path.join(output, asset)); - const original = source.startsWith("components/") - ? fs.readFileSync(path.join(ROOT_DIR, source)) - : /^(blame|find-file|organization|profile-|user-profile)/.test(source) || source === "wiki.css" - ? fs.readFileSync(path.join(ROOT_DIR, "styles/pages", source)) - : fs.readFileSync(path.join(ROOT_DIR, "public", source)); + const original = fs.readFileSync(path.join(ROOT_DIR, source)); expect(code.equals(original), `changed component asset: ${asset}`).toBe(true); expect(() => transform({ code, filename: asset })).not.toThrow(); expect(templates).toContain(`/css/${asset}`); } }); +it("links every page stylesheet from the component assets", () => { + // The bundle ships every styles/pages stylesheet; this link is the only place a new one has to be added. + const assets = fs.readFileSync(path.join(ROOT_DIR, "templates", "base", "component_assets.tmpl"), "utf-8"); + const pages = fs.readdirSync(path.join(ROOT_DIR, "styles", "pages")).filter(name => name.endsWith(".css")); + expect(pages.length).toBeGreaterThan(0); + for (const page of pages) { + expect(assets, `unlinked page stylesheet: ${page}`).toContain( + `` + ); + } +}); + it("stamps every template asset link with the hash of the shipped file", async () => { const { createHash } = await import("node:crypto"); const output = path.join(DIST_DIR, "forgejo"); @@ -418,7 +427,7 @@ describe("Forgejo 15 native integration", () => { expect(css).toContain('--fonts-proportional:"Mona Sans VF", -apple-system, BlinkMacSystemFont'); expect(css).toContain('--fonts-monospace:"Monaspace Neon", ui-monospace, SFMono-Regular'); expect(css).not.toContain("@font-face"); - expect(fs.existsSync(path.join(DIST_DIR, "assets", "fonts"))).toBe(false); + expect(fs.existsSync(path.join(DIST_DIR, "forgejo", "public", "assets", "fonts"))).toBe(false); }); it("matches GitHub's measured repository navigation geometry", () => { @@ -481,9 +490,12 @@ describe("Forgejo 15 native integration", () => { expect(navbar).not.toContain('id="navbar-expand-toggle"'); expect(navbar).toContain('href="{{AssetUrlPrefix}}/css/navbar-drawer.css"'); expect(navbar).toContain('src="{{AssetUrlPrefix}}/js/navbar-drawer.js"'); - for (const asset of ["navbar-drawer.css", "assets/js/navbar-drawer.js"]) { - expect(fs.readFileSync(path.join(DIST_DIR, asset), "utf-8")).toBe( - fs.readFileSync(path.join(ROOT_DIR, "public", asset), "utf-8") + for (const [asset, source] of [ + ["css/navbar-drawer.css", "navbar-drawer.css"], + ["js/navbar-drawer.js", "assets/js/navbar-drawer.js"], + ]) { + expect(fs.readFileSync(path.join(DIST_DIR, "forgejo", "public", "assets", asset), "utf-8")).toBe( + fs.readFileSync(path.join(ROOT_DIR, "public", source), "utf-8") ); } }); @@ -708,7 +720,9 @@ describe("Forgejo 15 native integration", () => { expect(wiki("revision")).toContain('{{template "repo/commit_list_item" dict "Commit" . "Page" $}}'); expect(read("components", "common", "editor", "editor.css")).toContain(".page-content.repository.wiki.new"); expect(read("templates", "base", "component_assets.tmpl")).toContain("/css/components/pages/wiki.css"); - expect(read("scripts", "assembleForgejo.ts")).toContain('"wiki.css"'); + expect( + fs.existsSync(path.join(DIST_DIR, "forgejo", "public", "assets", "css", "components", "pages", "wiki.css")) + ).toBe(true); }); it("lists branches in GitHub's branch tables with Forgejo's branch actions", () => { diff --git a/vite.config.ts b/vite.config.ts index de702a3b..af6dae79 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -21,4 +21,5 @@ import { giteaGitHubTheme } from "@lutinglt/gitea-github-theme/vite-plugin"; import { defineConfig } from "vite"; import themeConfig from "./theme.config.ts"; -export default defineConfig({ plugins: [giteaGitHubTheme(themeConfig)] }); +// dist/ holds only the built themes; scripts/assembleForgejo.ts copies public/ into the Forgejo tree itself. +export default defineConfig({ plugins: [giteaGitHubTheme(themeConfig)], publicDir: false }); From 1fed0d9960c87301cd909bc29c4d7430c5da37ad Mon Sep 17 00:00:00 2001 From: whereareiam Date: Wed, 28 Oct 2026 17:20:18 +0100 Subject: [PATCH 2/4] fix(preview): stamp a preview copy of the build, not dist The preview added ?preview= to the auto themes' imports in dist/forgejo itself, so src/build-output.test.ts failed after any dev:forgejo session until the next bun bundle. The stamp also went after the path, before the content version the bundle had already appended, which made links with two question marks such as theme-github-dark.css?preview=?v=. ThemeSynchronizer now copies the built css and js into dev/.generated/public/assets, and the reloader stamps that copy and writes preview-revision.json there. The Compose file mounts it instead of dist, and a sync recreates the container rather than restarting it, so a preview started before this change picks up the new mounts. The revision joins any existing query with &, keeping one query per link. The fixture README explains how to sign in as sam, whose password is preview-fixture like every fixture user other than alex. Co-Authored-By: Claude Opus 5.5 --- ARCHITECTURE.md | 7 +- README.md | 3 +- dev/docker-compose.yml | 4 +- dev/fixtures/README.md | 14 +++ dev/preview/PreviewApplication.ts | 18 ++-- dev/preview/config/PreviewConfig.ts | 4 + dev/preview/forgejo/ForgejoCompose.ts | 5 +- dev/preview/theme/PreviewReloader.ts | 31 ++++-- dev/preview/theme/ThemeSynchronizer.ts | 25 ++++- src/preview-reloader.test.ts | 137 ++++++++++++++++++------- 10 files changed, 184 insertions(+), 64 deletions(-) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index a68bd340..8c499807 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -21,9 +21,10 @@ routes; `components/project` contains the project list shared by repository, org `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. Every stylesheet in `styles/pages` ships as `css/components/pages/.css`; a new one only -needs its `` in `templates/base/component_assets.tmpl`. +installable Forgejo paths. The local preview mounts a copy of that assembled tree from `dev/.generated`, where it adds +its sign-in and reload hooks, so the same output is exercised during development and packaging and `dist` stays as +built. Every stylesheet in `styles/pages` ships as `css/components/pages/.css`; a new one only needs its `` +in `templates/base/component_assets.tmpl`. ## Architecture Layers diff --git a/README.md b/README.md index cca38e65..bf713f8f 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/dev/docker-compose.yml b/dev/docker-compose.yml index 067137d8..693fcd19 100644 --- a/dev/docker-compose.yml +++ b/dev/docker-compose.yml @@ -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: diff --git a/dev/fixtures/README.md b/dev/fixtures/README.md index db1b3520..b514542a 100644 --- a/dev/fixtures/README.md +++ b/dev/fixtures/README.md @@ -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. diff --git a/dev/preview/PreviewApplication.ts b/dev/preview/PreviewApplication.ts index 6cbbb2ee..780a94e3 100644 --- a/dev/preview/PreviewApplication.ts +++ b/dev/preview/PreviewApplication.ts @@ -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); @@ -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; @@ -67,8 +66,7 @@ export class PreviewApplication { private async start(): Promise { this.builder.build(); - this.autoLogin.prepare(); - this.reloader.prepare(); + this.prepare(); this.compose.start(); await this.readiness.wait(); this.reloader.publish(); @@ -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 { 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."); diff --git a/dev/preview/config/PreviewConfig.ts b/dev/preview/config/PreviewConfig.ts index eefca553..daeca36f 100644 --- a/dev/preview/config/PreviewConfig.ts +++ b/dev/preview/config/PreviewConfig.ts @@ -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[]; @@ -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)); diff --git a/dev/preview/forgejo/ForgejoCompose.ts b/dev/preview/forgejo/ForgejoCompose.ts index 84fac2c2..892c027f 100644 --- a/dev/preview/forgejo/ForgejoCompose.ts +++ b/dev/preview/forgejo/ForgejoCompose.ts @@ -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 { diff --git a/dev/preview/theme/PreviewReloader.ts b/dev/preview/theme/PreviewReloader.ts index e7aee947..00265602 100644 --- a/dev/preview/theme/PreviewReloader.ts +++ b/dev/preview/theme/PreviewReloader.ts @@ -12,14 +12,15 @@ export class PreviewReloader { this.config = config; } + /** Stamps the copy that ThemeSynchronizer staged, never the build in dist. */ public prepare(): void { this.revision = randomUUID(); - const dist = join(this.config.projectDirectory, "dist/forgejo/public/assets/css"); - for (const name of readdirSync(dist).filter(name => name.endsWith("-auto.css"))) { - const file = join(dist, name); + const stylesheets = this.stylesheets(); + for (const name of readdirSync(stylesheets).filter(name => name.endsWith("-auto.css"))) { + const file = join(stylesheets, name); const css = readFileSync(file, "utf8").replace( - /(theme-github-[^"?]+\.css)(?:\?preview=[^" ]*)?/g, - `$1?preview=${this.revision}` + /(@import ")(\.\/theme-[^"?]+\.css(?:\?[^"]*)?)"/g, + (_match, start: string, url: string) => `${start}${this.stamp(url)}"` ); writeFileSync(file, css); } @@ -30,11 +31,15 @@ export class PreviewReloader { /** Publish only after Forgejo has restarted with the new templates. */ public publish(): void { - const file = join(this.config.projectDirectory, "dist/forgejo/public/assets/css", "preview-revision.json"); + const file = join(this.stylesheets(), "preview-revision.json"); writeFileSync(`${file}.tmp`, JSON.stringify({ revision: this.revision })); renameSync(`${file}.tmp`, file); } + private stylesheets(): string { + return join(this.config.generatedAssetsDirectory, "css"); + } + private versionTemplateAssets(directory: string): void { if (!existsSync(directory)) return; for (const entry of readdirSync(directory, { withFileTypes: true })) { @@ -45,14 +50,24 @@ export class PreviewReloader { writeFileSync( file, template.replace( - /((?:src="{{AssetUrlPrefix}}\/js\/[^"?]+\.js|href="{{AssetUrlPrefix}}\/css\/[^"?]+\.css))(?:\?preview=[^"]*)?/g, - `$1?preview=${this.revision}` + /((?:src|href)=")(\{\{AssetUrlPrefix\}\}\/(?:js\/[^"?]+\.js|css\/[^"?]+\.css)(?:\?[^"]*)?)"/g, + (_match, start: string, url: string) => `${start}${this.stamp(url)}"` ) ); } } } + /** Sets the preview parameter beside the content version the bundle already appended, as one query. */ + private stamp(url: string): string { + const [path, ...query] = url.split("?"); + const parameters = query + .join("?") + .split("&") + .filter(parameter => parameter && !parameter.startsWith("preview=")); + return `${path}?${[...parameters, `preview=${this.revision}`].join("&")}`; + } + private script(): string { return `'; - writeFileSync(join(root, "dist/forgejo/templates", "repo", "commit.tmpl"), template); - writeFileSync( - join(root, "dist/forgejo/public/assets/css", "theme-github-auto.css"), - '@import "./theme-github-dark.css" (prefers-color-scheme: dark);\n@import "./theme-github-light.css" (prefers-color-scheme: light);' - ); - const config = new PreviewConfig(root, { FORGEJO_PREVIEW_AUTO_LOGIN: "false" }); - const reload = new PreviewReloader(config); +/** A dist/forgejo as `bun bundle` leaves it, with content versions on the imports and links. */ +const project = () => { + const root = mkdtempSync(join(tmpdir(), "theme-preview-")); + directories.push(root); + const files = { + "dist/forgejo/templates/repo/commit.tmpl": + '\n', + "dist/forgejo/templates/base/head_style.tmpl": + '', + "dist/forgejo/public/assets/css/theme-github-auto.css": + '@import "./theme-github-dark.css?v=1111111111" (prefers-color-scheme: dark);\n@import "./theme-github-light.css?v=2222222222" (prefers-color-scheme: light);', + "dist/forgejo/public/assets/css/theme-github-dark.css": ":root{}", + "dist/forgejo/public/assets/js/diff-view.js": "export {};", + }; + for (const [file, content] of Object.entries(files)) { + mkdirSync(dirname(join(root, file)), { recursive: true }); + writeFileSync(join(root, file), content); + } + const config = new PreviewConfig(root, { FORGEJO_PREVIEW_AUTO_LOGIN: "false" }); + const synchronizer = new ThemeSynchronizer(config, new ForgejoCompose(config, new CommandRunner(root))); + const reloader = new PreviewReloader(config); + const prepare = () => { new AutoLoginTemplate(config).prepare(); - reload.prepare(); - expect(existsSync(join(root, "dist/forgejo/public/assets/css", "preview-revision.json"))).toBe(false); - reload.publish(); - const first = JSON.parse( - readFileSync(join(root, "dist/forgejo/public/assets/css", "preview-revision.json"), "utf8") - ).revision; - const css = readFileSync(join(root, "dist/forgejo/public/assets/css", "theme-github-auto.css"), "utf8"); - expect(css).toContain(`theme-github-dark.css?preview=${first}`); - expect(css).toContain(`theme-github-light.css?preview=${first}`); - expect(readFileSync(join(root, "dist/forgejo/templates", "repo", "commit.tmpl"), "utf8")).toBe(template); - expect(readFileSync(join(config.generatedTemplatesDirectory, "repo", "commit.tmpl"), "utf8")).toContain( - `diff-view.js?preview=${first}` - ); + synchronizer.stage(); + reloader.prepare(); + }; + const staged = (file: string) => readFileSync(join(config.generatedAssetsDirectory, file), "utf8"); + const template = (file: string) => readFileSync(join(config.generatedTemplatesDirectory, file), "utf8"); + const revision = () => JSON.parse(staged("css/preview-revision.json")).revision as string; + return { root, files, config, reloader, prepare, staged, template, revision }; +}; + +describe("local preview reload", () => { + it("stamps its own copy of the build and leaves dist as `bun bundle` wrote it", () => { + const { root, files, config, reloader, prepare, staged, revision } = project(); + prepare(); + reloader.publish(); + for (const [file, content] of Object.entries(files)) + expect(readFileSync(join(root, file), "utf8"), file).toBe(content); + expect(existsSync(join(root, "dist/forgejo/public/assets/css/preview-revision.json"))).toBe(false); + expect(staged("css/theme-github-dark.css")).toBe(":root{}"); + expect(staged("js/diff-view.js")).toBe("export {};"); + expect(staged("css/theme-github-auto.css")).toContain(`theme-github-dark.css?v=1111111111&preview=${revision()}"`); expect(readFileSync(config.autoLoginTemplateFile, "utf8")).toContain('cache: "no-store"'); - new AutoLoginTemplate(config).prepare(); - reload.prepare(); - expect( - JSON.parse(readFileSync(join(root, "dist/forgejo/public/assets/css", "preview-revision.json"), "utf8")).revision - ).toBe(first); - reload.publish(); - const second = JSON.parse( - readFileSync(join(root, "dist/forgejo/public/assets/css", "preview-revision.json"), "utf8") - ).revision; + }); + + it("joins the preview revision to the content version as one query", () => { + const { reloader, prepare, staged, template, revision } = project(); + prepare(); + reloader.publish(); + const first = revision(); + expect(template("repo/commit.tmpl")).toContain(`/js/diff-view.js?v=0123456789&preview=${first}"`); + expect(template("repo/commit.tmpl")).toContain(`/css/site-footer.css?v=abcdef0123&preview=${first}"`); + expect(template("base/head_style.tmpl")).toContain(`.css?v={{AssetVersion}}-fedcba9876&preview=${first}"`); + const urls = [ + ...staged("css/theme-github-auto.css").matchAll(/@import "([^"]+)"/g), + ...template("repo/commit.tmpl").matchAll(/(?:src|href)="([^"]+)"/g), + ].map(([, url]) => url); + expect(urls).toHaveLength(4); + for (const url of urls) expect(url.split("?"), url).toHaveLength(2); + }); + + it("publishes a new revision only when ready and drops the previous one and its files", () => { + const { config, reloader, prepare, staged, template, revision } = project(); + expect(() => revision()).toThrow(); + prepare(); + expect(() => revision()).toThrow(); + reloader.publish(); + const first = revision(); + writeFileSync(join(config.generatedAssetsDirectory, "css/removed.css"), ""); + prepare(); + expect(() => revision()).toThrow(); + expect(existsSync(join(config.generatedAssetsDirectory, "css/removed.css"))).toBe(false); + reloader.publish(); + const second = revision(); expect(second).not.toBe(first); - expect(readFileSync(join(root, "dist/forgejo/public/assets/css", "theme-github-auto.css"), "utf8")).not.toContain( - first + expect(staged("css/theme-github-auto.css")).not.toContain(first); + expect(template("repo/commit.tmpl")).not.toContain(first); + }); + + it("mounts the staged copy and generated templates into Forgejo, never dist", () => { + const config = new PreviewConfig(ROOT_DIR); + const compose = readFileSync(config.composeFile, "utf8"); + const mounts = Object.fromEntries( + [...compose.matchAll(/^\s+- (\.{1,2}\/\S+):(\/var\/lib\/gitea\/custom\/\S+):ro$/gm)].map( + ([, host, container]) => [container, resolve(dirname(config.composeFile), host)] + ) ); + expect(mounts).toEqual({ + "/var/lib/gitea/custom/public/assets/css": join(config.generatedAssetsDirectory, "css"), + "/var/lib/gitea/custom/public/assets/js": join(config.generatedAssetsDirectory, "js"), + "/var/lib/gitea/custom/templates": config.generatedTemplatesDirectory, + }); + expect(compose).not.toContain("dist/"); + expect(relative(ROOT_DIR, config.generatedAssetsDirectory).startsWith("dev/.generated/")).toBe(true); }); }); From c209a0ee04c3ed7b7eaa6bc03dadc2d7f220f81d Mon Sep 17 00:00:00 2001 From: whereareiam Date: Wed, 28 Oct 2026 17:19:24 +0100 Subject: [PATCH 3/4] feat(templates): add bun drift to compare overrides with forgejo The template overrides are copies of Forgejo's templates, but nothing recorded which release they came from, so an upgrade showed no sign of the overrides that fell behind. The forgejo field of package.json now pins that release. The preview derives its image from it, which also replaces the 16.0.3 image its docker run commands still named, and a test checks that docker-compose.yml runs the same release. bun drift checks out the templates of a release into node_modules/.cache/forgejo with a sparse git clone and lists each override by the lines the theme added and removed, then the templates Forgejo does not have. Given two releases, it lists the overridden templates Forgejo changed between them with Forgejo's diff, and the ones it added or removed, so a maintainer knows what to port. CONTRIBUTING.md describes the steps in a new Upgrading Forgejo section. Co-Authored-By: Claude Opus 5.5 --- CONTRIBUTING.md | 18 +++ dev/preview/config/constants.test.ts | 13 +- dev/preview/config/constants.ts | 5 +- package.json | 2 + scripts/templateDrift.test.ts | 111 ++++++++++++++ scripts/templateDrift.ts | 216 +++++++++++++++++++++++++++ vitest.config.ts | 2 +- 7 files changed, 359 insertions(+), 8 deletions(-) create mode 100644 scripts/templateDrift.test.ts create mode 100644 scripts/templateDrift.ts diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ee704b80..2929997e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -136,6 +136,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. diff --git a/dev/preview/config/constants.test.ts b/dev/preview/config/constants.test.ts index a1e154dd..ebc64aac 100644 --- a/dev/preview/config/constants.test.ts +++ b/dev/preview/config/constants.test.ts @@ -1,12 +1,13 @@ import { readFileSync } from "node:fs"; -import { fileURLToPath } from "node:url"; +import { join } from "node:path"; import { expect, it } from "vitest"; +import pkg from "../../../package.json"; import { COMPOSE } from "./constants.ts"; -it("runs its one-off containers on the image of the Compose service", () => { - // The ownership repair and database cleanup start the image by name; another version would be pulled beside it. - const compose = readFileSync(fileURLToPath(new URL(`../../../${COMPOSE.file}`, import.meta.url)), "utf-8"); - const service = compose.slice(compose.indexOf(` ${COMPOSE.service}:\n`)); - expect(service.match(/^ {4}image: (\S+)$/m)?.[1]).toBe(COMPOSE.image); +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); }); diff --git a/dev/preview/config/constants.ts b/dev/preview/config/constants.ts index 78931200..da3fde5f 100644 --- a/dev/preview/config/constants.ts +++ b/dev/preview/config/constants.ts @@ -1,3 +1,5 @@ +import pkg from "../../../package.json"; + export const COMMANDS = { bun: "bun", docker: "docker", @@ -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:17.0.0-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 = { diff --git a/package.json b/package.json index 1b3a7b12..d36a5bd1 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "engines": { "bun": ">=1.3.14" }, + "forgejo": "17.0.0", "exports": { "./core": "./src/index.ts", "./vite-plugin": "./src/vite-plugin/index.ts", @@ -36,6 +37,7 @@ "bundle:ci": "bun bundle && bun vitest", "sync:theme": "bun scripts/syncTheme.ts", "update:primer": "bun scripts/buildPrimerTSToken.ts", + "drift": "bun scripts/templateDrift.ts", "update:snapshot": "bun vitest --update", "install:clean": "rm -rf node_modules bun.lock bun.lockb && bun install" }, diff --git a/scripts/templateDrift.test.ts b/scripts/templateDrift.test.ts new file mode 100644 index 00000000..64b2973b --- /dev/null +++ b/scripts/templateDrift.test.ts @@ -0,0 +1,111 @@ +import { describe, expect, it } from "vitest"; + +import { + formatChangeReport, + formatDiffSize, + formatDriftReport, + parseDriftArguments, + parseForgejoVersion, + parseNumstat, +} from "./templateDrift.ts"; + +describe("parseForgejoVersion", () => { + it("names the release tag with or without the v", () => { + expect(parseForgejoVersion("17.0.0")).toEqual({ version: "17.0.0", tag: "v17.0.0" }); + expect(parseForgejoVersion("v17.1.2")).toEqual({ version: "17.1.2", tag: "v17.1.2" }); + expect(parseForgejoVersion("18.0.0-rc0")).toEqual({ version: "18.0.0-rc0", tag: "v18.0.0-rc0" }); + }); + + it("rejects anything that is not a full release", () => { + for (const input of ["17", "17.0", "latest", "v17.0.0-", "17.0.0-rootless/x", ""]) { + expect(() => parseForgejoVersion(input)).toThrow(/not a Forgejo release/); + } + }); +}); + +describe("parseDriftArguments", () => { + it("compares against the pinned release by default", () => { + expect(parseDriftArguments([], "17.0.0")).toEqual({ base: { version: "17.0.0", tag: "v17.0.0" } }); + }); + + it("takes a second release to compare Forgejo's changes", () => { + const { base, next } = parseDriftArguments(["17.0.0", "17.1.0"], "16.0.0"); + expect(base.tag).toBe("v17.0.0"); + expect(next?.tag).toBe("v17.1.0"); + }); + + it("refuses more than two releases", () => { + expect(() => parseDriftArguments(["1.0.0", "2.0.0", "3.0.0"], "17.0.0")).toThrow(/Usage/); + }); +}); + +describe("parseNumstat", () => { + it("reads the lines added and removed", () => { + expect(parseNumstat("12\t3\ta.tmpl => b.tmpl\n")).toEqual({ added: 12, removed: 3 }); + }); + + it("reads no output as identical files", () => { + expect(parseNumstat("")).toEqual({ added: 0, removed: 0 }); + }); + + it("fails on binary files and several files", () => { + expect(() => parseNumstat("-\t-\ta.png\n")).toThrow(/Unexpected/); + expect(() => parseNumstat("1\t2\ta\n3\t4\tb\n")).toThrow(/Unexpected/); + }); +}); + +describe("formatDiffSize", () => { + it("shows the lines added and removed", () => { + expect(formatDiffSize({ added: 4, removed: 0 })).toBe("+4 -0"); + expect(formatDiffSize({ added: 0, removed: 0 })).toBe("identical"); + }); +}); + +describe("formatDriftReport", () => { + it("lists the largest overrides first, then the theme's own templates", () => { + const report = formatDriftReport(parseForgejoVersion("17.0.0"), [ + { path: "repo/home.tmpl", size: { added: 2, removed: 1 } }, + { path: "user/profile_empty.tmpl" }, + { path: "base/head_navbar.tmpl", size: { added: 120, removed: 80 } }, + { path: "repo/header.tmpl", size: { added: 0, removed: 0 } }, + ]); + expect(report).toBe( + [ + "3 templates overriding Forgejo v17.0.0, by lines the theme added and removed:", + " +120 -80 base/head_navbar.tmpl", + " +2 -1 repo/home.tmpl", + " identical repo/header.tmpl", + "", + "1 template without a file in Forgejo v17.0.0; check that none replaces a renamed one:", + " user/profile_empty.tmpl", + ].join("\n") + ); + }); +}); + +describe("formatChangeReport", () => { + const base = parseForgejoVersion("17.0.0"); + const next = parseForgejoVersion("17.1.0"); + + it("summarises the changes and prints Forgejo's diffs", () => { + const report = formatChangeReport(base, next, [ + { path: "repo/home.tmpl", kind: "changed", size: { added: 1, removed: 1 }, diff: "-old\n+new\n" }, + { path: "repo/gone.tmpl", kind: "removed" }, + ]); + expect(report).toBe( + [ + "Forgejo v17.0.0..v17.1.0 changed 2 templates the theme overrides:", + " removed repo/gone.tmpl", + " +1 -1 repo/home.tmpl", + "", + "-old\n+new", + ].join("\n") + ); + }); + + it("says when nothing needs porting", () => { + expect(formatChangeReport(base, next, [])).toBe( + "Forgejo v17.0.0..v17.1.0 changed none of the overridden templates." + ); + }); +}); diff --git a/scripts/templateDrift.ts b/scripts/templateDrift.ts new file mode 100644 index 00000000..c651b0ea --- /dev/null +++ b/scripts/templateDrift.ts @@ -0,0 +1,216 @@ +import { spawnSync } from "node:child_process"; +import { existsSync, mkdirSync, readdirSync, renameSync, rmSync } from "node:fs"; +import { dirname, join, relative } from "node:path"; +import { fileURLToPath } from "node:url"; +import { pkg } from "./utils"; + +const rootDir = join(dirname(fileURLToPath(import.meta.url)), ".."); + +const FORGEJO_REPOSITORY = "https://codeberg.org/forgejo/forgejo"; +const CACHE_DIRECTORY = join(rootDir, "node_modules", ".cache", "forgejo"); +const TEMPLATES_DIRECTORY = join(rootDir, "templates"); + +const USAGE = `Usage: bun drift [version] [next-version] + + bun drift how far each override is from the pinned Forgejo (${pkg.forgejo}) + bun drift 17.1.0 the same against another release + bun drift 17.0.0 17.1.0 the overridden templates Forgejo changed between two releases, with Forgejo's diff`; + +export interface ForgejoVersion { + version: string; + tag: string; +} + +export interface DriftArguments { + base: ForgejoVersion; + next?: ForgejoVersion; +} + +export interface DiffSize { + added: number; + removed: number; +} + +/** A theme template and its diff against Forgejo's file; no size means Forgejo has no such file. */ +export interface TemplateDrift { + path: string; + size?: DiffSize; +} + +/** How an overridden template changed upstream between two releases. */ +export type UpstreamChange = + | { path: string; kind: "changed"; size: DiffSize; diff: string } + | { path: string; kind: "added" | "removed" }; + +/** Accepts a release as `17.0.0` or `v17.0.0`, with an optional pre-release suffix such as `-rc0`. */ +export function parseForgejoVersion(input: string): ForgejoVersion { + const match = /^v?(\d+\.\d+\.\d+(?:-[0-9A-Za-z.]+)?)$/.exec(input.trim()); + if (!match) throw new Error(`"${input}" is not a Forgejo release such as 17.0.0`); + return { version: match[1], tag: `v${match[1]}` }; +} + +export function parseDriftArguments(args: readonly string[], pinned: string): DriftArguments { + if (args.length > 2) throw new Error(USAGE); + const [base = pinned, next] = args; + return { base: parseForgejoVersion(base), next: next === undefined ? undefined : parseForgejoVersion(next) }; +} + +/** Reads the line counts of `git diff --numstat` for a single file; no output means the files are identical. */ +export function parseNumstat(output: string): DiffSize { + const line = output.trim(); + if (line === "") return { added: 0, removed: 0 }; + const match = /^(\d+)\t(\d+)\t/.exec(line); + if (!match || line.includes("\n")) throw new Error(`Unexpected git diff --numstat output: ${line}`); + return { added: Number(match[1]), removed: Number(match[2]) }; +} + +export function formatDiffSize(size: DiffSize): string { + return size.added === 0 && size.removed === 0 ? "identical" : `+${size.added} -${size.removed}`; +} + +/** Lists the overrides furthest from Forgejo first, then the theme's own templates. */ +export function formatDriftReport(version: ForgejoVersion, drifts: readonly TemplateDrift[]): string { + const overrides = drifts + .filter((drift): drift is Required => drift.size !== undefined) + .sort((a, b) => lineCount(b.size) - lineCount(a.size) || a.path.localeCompare(b.path)); + const own = drifts.filter(drift => drift.size === undefined).map(drift => drift.path); + const width = Math.max(0, ...overrides.map(drift => formatDiffSize(drift.size).length)); + const lines = [ + `${templates(overrides.length)} overriding Forgejo ${version.tag}, by lines the theme added and removed:`, + ]; + lines.push(...overrides.map(drift => ` ${formatDiffSize(drift.size).padStart(width)} ${drift.path}`)); + if (own.length > 0) { + lines.push( + "", + `${templates(own.length)} without a file in Forgejo ${version.tag}; check that none replaces a renamed one:` + ); + lines.push(...own.sort().map(path => ` ${path}`)); + } + return lines.join("\n"); +} + +/** Summarises the upstream changes to port, then prints Forgejo's diff of each changed file. */ +export function formatChangeReport( + base: ForgejoVersion, + next: ForgejoVersion, + changes: readonly UpstreamChange[] +): string { + if (changes.length === 0) return `Forgejo ${base.tag}..${next.tag} changed none of the overridden templates.`; + const sorted = [...changes].sort((a, b) => a.path.localeCompare(b.path)); + const labels = sorted.map(change => (change.kind === "changed" ? formatDiffSize(change.size) : change.kind)); + const width = Math.max(...labels.map(label => label.length)); + const lines = [`Forgejo ${base.tag}..${next.tag} changed ${templates(changes.length)} the theme overrides:`]; + lines.push(...sorted.map((change, index) => ` ${labels[index].padStart(width)} ${change.path}`)); + for (const change of sorted) if (change.kind === "changed") lines.push("", change.diff.trimEnd()); + return lines.join("\n"); +} + +function templates(count: number): string { + return count === 1 ? "1 template" : `${count} templates`; +} + +function lineCount(size: DiffSize): number { + return size.added + size.removed; +} + +function listTemplates(directory: string, base = directory): string[] { + return readdirSync(directory, { withFileTypes: true }).flatMap(entry => { + const path = join(directory, entry.name); + return entry.isDirectory() ? listTemplates(path, base) : [relative(base, path)]; + }); +} + +/** `git diff --no-index` exits with 1 when the files differ, so only a higher status is a failure. */ +function gitDiff(directory: string, args: readonly string[]): string { + const result = spawnSync("git", ["diff", "--no-index", "--no-color", "--no-ext-diff", ...args], { + cwd: directory, + encoding: "utf8", + maxBuffer: 64 * 1024 * 1024, + }); + if (result.error) throw result.error; + if (result.status !== 0 && result.status !== 1) + throw new Error(`git diff ${args.join(" ")} failed:\n${result.stderr}`); + return result.stdout; +} + +function git(args: readonly string[]): void { + const result = spawnSync("git", args, { encoding: "utf8" }); + if (result.error) throw result.error; + if (result.status !== 0) throw new Error(`git ${args.join(" ")} failed:\n${result.stderr}`); +} + +/** Checks out only `templates/` of a release, once; a failed clone leaves no cache behind. */ +function fetchUpstream(version: ForgejoVersion): string { + const directory = join(CACHE_DIRECTORY, version.tag); + if (existsSync(join(directory, "templates"))) return directory; + const partial = `${directory}.partial-${process.pid}`; + mkdirSync(CACHE_DIRECTORY, { recursive: true }); + rmSync(partial, { recursive: true, force: true }); + console.error(`Fetching the templates of Forgejo ${version.tag} from ${FORGEJO_REPOSITORY}`); + try { + git([ + "clone", + "--quiet", + "--depth=1", + "--filter=blob:none", + "--sparse", + "--branch", + version.tag, + FORGEJO_REPOSITORY, + partial, + ]); + git(["-C", partial, "sparse-checkout", "set", "templates"]); + rmSync(directory, { recursive: true, force: true }); + renameSync(partial, directory); + } finally { + rmSync(partial, { recursive: true, force: true }); + } + return directory; +} + +function measureDrift(upstream: string): TemplateDrift[] { + return listTemplates(TEMPLATES_DIRECTORY).map(path => { + const upstreamFile = join(upstream, "templates", path); + if (!existsSync(upstreamFile)) return { path }; + return { path, size: parseNumstat(gitDiff(rootDir, ["--numstat", upstreamFile, join(TEMPLATES_DIRECTORY, path)])) }; + }); +} + +function listUpstreamChanges(base: ForgejoVersion, next: ForgejoVersion): UpstreamChange[] { + const files = (version: ForgejoVersion, path: string) => join(version.tag, "templates", path); + return listTemplates(TEMPLATES_DIRECTORY).flatMap((path): UpstreamChange[] => { + const before = existsSync(join(CACHE_DIRECTORY, files(base, path))); + const after = existsSync(join(CACHE_DIRECTORY, files(next, path))); + if (!before && !after) return []; + if (!before || !after) return [{ path, kind: after ? "added" : "removed" }]; + const pair = [files(base, path), files(next, path)]; + const size = parseNumstat(gitDiff(CACHE_DIRECTORY, ["--numstat", ...pair])); + if (lineCount(size) === 0) return []; + return [{ path, kind: "changed", size, diff: gitDiff(CACHE_DIRECTORY, ["--no-prefix", ...pair]) }]; + }); +} + +function main(): void { + const args = process.argv.slice(2); + if (args.includes("--help") || args.includes("-h")) { + console.log(USAGE); + return; + } + const { base, next } = parseDriftArguments(args, pkg.forgejo); + const upstream = fetchUpstream(base); + if (!next) { + console.log(formatDriftReport(base, measureDrift(upstream))); + return; + } + fetchUpstream(next); + console.log(formatChangeReport(base, next, listUpstreamChanges(base, next))); +} + +if (process.argv[1] === fileURLToPath(import.meta.url)) { + try { + main(); + } catch (error) { + console.error(error instanceof Error ? error.message : error); + process.exit(1); + } +} diff --git a/vitest.config.ts b/vitest.config.ts index fe2fe29c..6c4acb6b 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -21,7 +21,7 @@ import { defineConfig } from "vitest/config"; export default defineConfig({ test: { - include: ["src/**/*.test.ts", "components/**/*.test.ts", "dev/preview/**/*.test.ts"], + include: ["src/**/*.test.ts", "components/**/*.test.ts", "dev/preview/**/*.test.ts", "scripts/**/*.test.ts"], testTimeout: 20000, open: false, allowOnly: true, From b82f91dc6cb8e6e25b85b4c1bfee5e6198f9140b Mon Sep 17 00:00:00 2001 From: whereareiam Date: Wed, 28 Oct 2026 17:21:06 +0100 Subject: [PATCH 4/4] docs(architecture): describe the forgejo theme and where styles go ARCHITECTURE.md still described the Gitea CSS generator, with a short Forgejo section on top and a single-file Gitea constraint. It now describes the two parts the theme ships: the theme stylesheets built from the palette pipeline and styles/, and the template pack that scripts/assembleForgejo.ts puts together from templates, components, page CSS and public assets. It covers asset versions, the local preview and the release archives, and the palette pipeline is kept, shorter. CONTRIBUTING.md gains where new styles go (styles/ for Forgejo's own markup, components/ for reusable UI, styles/pages for one page, public/ for an asset one template loads), the class prefixes and the z-index scale from motion.css. Its directory table lists components, page CSS, public and dev, and stale references now name component_assets.tmpl, the sidebar toggle component, results.js and the select example. Co-Authored-By: Claude Opus 5.5 --- ARCHITECTURE.md | 728 ++++++++++++------------------------------------ CONTRIBUTING.md | 114 +++++--- 2 files changed, 261 insertions(+), 581 deletions(-) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 8c499807..85dce3ad 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -1,241 +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 a copy of that assembled tree from `dev/.generated`, where it adds -its sign-in and reload hooks, so the same output is exercised during development and packaging and `dist` stays as -built. Every stylesheet in `styles/pages` ships as `css/components/pages/.css`; a new one only needs its `` -in `templates/base/component_assets.tmpl`. - -## 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 | | ---------------------------------------------- | -------------------------------------- | @@ -245,372 +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 -│ └── 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 2929997e..18fc9d0e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,34 +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) | -| 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. @@ -112,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: @@ -233,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`. @@ -242,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 @@ -295,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 `