From 0ff61974ffc1a3f00834c6e136c4cbd339c25c09 Mon Sep 17 00:00:00 2001 From: chuan Date: Sat, 16 May 2026 02:09:59 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=88=9D=E5=A7=8B=E5=8C=96=E5=85=A8?= =?UTF-8?q?=E9=83=A8=E6=A8=A1=E6=9D=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 4 ++ AGENTS.md | 0 README.md | 22 ++++++++++ bun.lock | 53 +++++++++++++++++++++++ compose.dev.yaml | 21 +++++++++ docs/rules.md | 69 ++++++++++++++++++++++++++++++ package.json | 23 ++++++++++ scripts/build.ts | 54 +++++++++++++++++++++++ scripts/preview.ts | 42 ++++++++++++++++++ scripts/verify-docker.ts | 33 ++++++++++++++ scripts/verify-theme.ts | 21 +++++++++ src/css.ts | 17 ++++++++ src/theme-types.ts | 11 +++++ styles/components/index.ts | 26 +++++++++++ styles/index.ts | 6 +++ styles/pages/index.ts | 22 ++++++++++ styles/primitives/index.ts | 51 ++++++++++++++++++++++ styles/tokens/index.ts | 4 ++ templates/README.md | 0 templates/base/footer_content.tmpl | 1 + themes/light.ts | 46 ++++++++++++++++++++ tsconfig.json | 12 ++++++ 22 files changed, 538 insertions(+) create mode 100644 .gitignore create mode 100644 AGENTS.md create mode 100644 README.md create mode 100644 bun.lock create mode 100644 compose.dev.yaml create mode 100644 docs/rules.md create mode 100644 package.json create mode 100644 scripts/build.ts create mode 100644 scripts/preview.ts create mode 100644 scripts/verify-docker.ts create mode 100644 scripts/verify-theme.ts create mode 100644 src/css.ts create mode 100644 src/theme-types.ts create mode 100644 styles/components/index.ts create mode 100644 styles/index.ts create mode 100644 styles/pages/index.ts create mode 100644 styles/primitives/index.ts create mode 100644 styles/tokens/index.ts create mode 100644 templates/README.md create mode 100644 templates/base/footer_content.tmpl create mode 100644 themes/light.ts create mode 100644 tsconfig.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d0355bc --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +.gitea +.gitea-data +dist +node_modules diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..e69de29 diff --git a/README.md b/README.md new file mode 100644 index 0000000..da7b6c6 --- /dev/null +++ b/README.md @@ -0,0 +1,22 @@ +# Gitea Theme Dev + +用于开发 Gitea 自定义主题样式和模板覆盖的工作区。 + +## 技术选型 + +当前项目使用 Bun + TypeScript token + Lightning CSS + Preview。 + +样式按 `tokens -> primitives -> components -> pages` 分层聚合,主题值放在 `themes/`,模板覆盖放在 `templates/`。详细规则见 `docs/rules.md`。 + +## 常用命令 + +```bash +bun install +bun run build +bun run build:min +bun run dev +bun run preview +bun run test +``` + +构建会生成 `dist/theme-gitea-auto.css`,并同步输出到 `.gitea/custom/public/assets/css/theme-gitea-auto.css` 供 Gitea custom 目录使用。 diff --git a/bun.lock b/bun.lock new file mode 100644 index 0000000..4d0d2f5 --- /dev/null +++ b/bun.lock @@ -0,0 +1,53 @@ +{ + "lockfileVersion": 1, + "configVersion": 1, + "workspaces": { + "": { + "name": "gitea-theme-dev", + "devDependencies": { + "@types/bun": "latest", + "lightningcss": "^1.29.2", + "polished": "^4.3.1", + }, + }, + }, + "packages": { + "@babel/runtime": ["@babel/runtime@7.29.2", "https://registry.npmmirror.com/@babel/runtime/-/runtime-7.29.2.tgz", {}, "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g=="], + + "@types/bun": ["@types/bun@1.3.14", "https://registry.npmmirror.com/@types/bun/-/bun-1.3.14.tgz", { "dependencies": { "bun-types": "1.3.14" } }, "sha512-h1hFqFVcvAvD9j9K7ZW7vd82aSA+rTdznZa+5bwvCwqSB1jmmfLcbIWhOLx1/+boy/xmjgCs/OMUL8hRJSmnPw=="], + + "@types/node": ["@types/node@25.8.0", "https://registry.npmmirror.com/@types/node/-/node-25.8.0.tgz", { "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } }, "sha512-TCFSk8IZh+iLX1xtksoBVtdmgL+1IX0fC9BeU4QqFSuNdN/K+HUlhqOzEmSYYpZUVsLYcPqc9KX+60iDuninSQ=="], + + "bun-types": ["bun-types@1.3.14", "https://registry.npmmirror.com/bun-types/-/bun-types-1.3.14.tgz", { "dependencies": { "@types/node": "*" } }, "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ=="], + + "detect-libc": ["detect-libc@2.1.2", "https://registry.npmmirror.com/detect-libc/-/detect-libc-2.1.2.tgz", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], + + "lightningcss": ["lightningcss@1.32.0", "https://registry.npmmirror.com/lightningcss/-/lightningcss-1.32.0.tgz", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="], + + "lightningcss-android-arm64": ["lightningcss-android-arm64@1.32.0", "https://registry.npmmirror.com/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", { "os": "android", "cpu": "arm64" }, "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg=="], + + "lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.32.0", "https://registry.npmmirror.com/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", { "os": "darwin", "cpu": "arm64" }, "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ=="], + + "lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.32.0", "https://registry.npmmirror.com/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", { "os": "darwin", "cpu": "x64" }, "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w=="], + + "lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.32.0", "https://registry.npmmirror.com/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", { "os": "freebsd", "cpu": "x64" }, "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig=="], + + "lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.32.0", "https://registry.npmmirror.com/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", { "os": "linux", "cpu": "arm" }, "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw=="], + + "lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.32.0", "https://registry.npmmirror.com/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", { "os": "linux", "cpu": "arm64" }, "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ=="], + + "lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.32.0", "https://registry.npmmirror.com/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", { "os": "linux", "cpu": "arm64" }, "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg=="], + + "lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.32.0", "https://registry.npmmirror.com/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", { "os": "linux", "cpu": "x64" }, "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA=="], + + "lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.32.0", "https://registry.npmmirror.com/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", { "os": "linux", "cpu": "x64" }, "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg=="], + + "lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.32.0", "https://registry.npmmirror.com/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", { "os": "win32", "cpu": "arm64" }, "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw=="], + + "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.32.0", "https://registry.npmmirror.com/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", { "os": "win32", "cpu": "x64" }, "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q=="], + + "polished": ["polished@4.3.1", "https://registry.npmmirror.com/polished/-/polished-4.3.1.tgz", { "dependencies": { "@babel/runtime": "^7.17.8" } }, "sha512-OBatVyC/N7SCW/FaDHrSd+vn0o5cS855TOmYi4OkdWUMSJCET/xip//ch8xGUvtr3i44X9LVyWwQlRMTN3pwSA=="], + + "undici-types": ["undici-types@7.24.6", "https://registry.npmmirror.com/undici-types/-/undici-types-7.24.6.tgz", {}, "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg=="], + } +} diff --git a/compose.dev.yaml b/compose.dev.yaml new file mode 100644 index 0000000..cfab80c --- /dev/null +++ b/compose.dev.yaml @@ -0,0 +1,21 @@ +services: + server: + image: docker.gitea.com/gitea:1.26.1 + container_name: gitea-theme-dev + environment: + - USER_UID=1000 + - USER_GID=1000 + - GITEA__server__LANDING_PAGE=explore + - GITEA__service_0x2E_explore__DISABLE_USERS_PAGE=true + - GITEA__service_0x2E_explore__DISABLE_ORGANIZATIONS_PAGE=true + - GITEA__service_0x2E_explore__DISABLE_CODE_PAGE=true + - GITEA__ui__THEMES=gitea-auto + - GITEA__ui__DEFAULT_THEME=gitea-auto + restart: always + volumes: + - ./.gitea-data:/data + - ./.gitea/custom/public:/data/gitea/public + - ./.gitea/custom/templates:/data/gitea/templates + ports: + - "3000:3000" + - "222:22" diff --git a/docs/rules.md b/docs/rules.md new file mode 100644 index 0000000..13afef6 --- /dev/null +++ b/docs/rules.md @@ -0,0 +1,69 @@ +# Gitea 主题项目规则 + +## 项目目标 + +本项目用于创建 Gitea 使用的主题样式和模板覆盖。样式使用 Bun + TypeScript token + Lightning CSS + Preview 的组合:TypeScript 负责组织 token 和样式片段,Lightning CSS 负责最终 CSS 转换和压缩,Preview 用于快速查看基础效果。 + +## 目录职责 + +`themes/` 放颜色主题和主题 token,例如 `light.ts`。主题文件只定义值,不写 Gitea 选择器。 + +`styles/tokens/` 放最底层设计变量,例如颜色、字号、间距、圆角、阴影、动效时长。这里不应该出现具体页面选择器。 + +`styles/primitives/` 放全站基础控件规则,例如 `.ui.button`、`.ui.input`、`input[type="checkbox"]`、`.ui.dropdown`、`.tippy-box`。这一层可以使用 token,但不应该处理具体页面布局。 + +`styles/components/` 放 Gitea 可复用业务组件样式,例如顶部导航、仓库列表、热力图、动态列表、登录表单、clone 面板。它比 primitive 更具体,但还不是页面级。 + +`styles/pages/` 放某个页面的布局修复和局部覆盖,例如 dashboard 首页、explore 仓库页、org/create 页面、登录页布局。这里可以写具体页面选择器,但改动范围必须尽量局部。 + +`templates/` 放 Gitea 模板覆盖文件。模板修改应尽量保持最小化,避免把样式逻辑写进模板。 + +`src/` 放构建辅助代码、类型和聚合逻辑。这里不直接承载某个具体页面的样式规则。 + +`.gitea/` 放 Gitea custom 目录和源代码参考资源。构建产物会输出到 `.gitea/custom/public/assets/css/theme-gitea-auto.css`。 + +`dist/` 放本地构建产物,主要用于检查和 Preview。 + +## 分层原则 + +一个规则只应该属于一个层级。按钮、输入框、下拉菜单等基础控件问题放到 `primitives/`;仓库列表、导航栏、动态流等可复用业务块放到 `components/`;只在某个页面成立的间距、布局和覆盖放到 `pages/`。 + +低层不能依赖高层。`tokens/` 不能依赖 `primitives/`、`components/` 或 `pages/`;`primitives/` 不能依赖页面结构;`components/` 不应该修复具体页面布局。 + +避免重复定义颜色、阴影、圆角和间距。优先在 `themes/` 与 `styles/tokens/` 中新增 token,再在其他层使用 `var(--gt-*)`。 + +页面级选择器必须足够具体,避免影响全站控件。例如登录页按钮间距问题应该放在登录页或登录组件范围内,不应该修改所有 `.ui.button`。 + +## 命名规则 + +自定义 CSS 变量统一使用 `--gt-*` 前缀,例如 `--gt-color-accent`、`--gt-radius-md`、`--gt-space-lg`。 + +主题名与 Gitea CSS 文件名保持一致。当前默认主题名是 `gitea-auto`,输出文件是 `theme-gitea-auto.css`。 + +样式模块默认导出字符串常量,由 `styles/index.ts` 按 `tokens -> primitives -> components -> pages` 顺序聚合。 + +## 构建与预览 + +使用 `bun run build` 构建未压缩 CSS。 + +使用 `bun run build:min` 构建压缩 CSS。 + +使用 `bun run dev` 监听样式、主题和 src 文件变化。 + +使用 `bun run preview` 启动本地预览页面。 + +使用 `bun run test` 构建并检查必要输出是否存在。 + +使用 `bun run compose:up` 启动 Gitea 开发容器,使用 `bun run compose:down` 停止容器。 + +## 修改流程 + +新增视觉值时,先判断是否应该成为 token。能复用的值放到 `themes/` 或 `styles/tokens/`。 + +修改控件外观时,优先检查 `styles/primitives/`,确保不会破坏页面级布局。 + +修改业务组件时,优先放到 `styles/components/`,并使用组件范围选择器约束影响面。 + +修改单个页面问题时,放到 `styles/pages/`,并写明页面级父选择器。 + +修改模板时,先确认 Gitea 原始模板结构,再把覆盖文件放入 `templates/` 或同步到 `.gitea/custom/templates/`。 diff --git a/package.json b/package.json new file mode 100644 index 0000000..f2cc426 --- /dev/null +++ b/package.json @@ -0,0 +1,23 @@ +{ + "name": "gitea-theme-dev", + "version": "0.1.0", + "private": true, + "type": "module", + "description": "Bun-powered custom theme workspace for Gitea.", + "scripts": { + "build": "bun scripts/build.ts", + "dev": "bun scripts/build.ts --watch", + "preview": "bun run build && bun scripts/preview.ts", + "build:min": "bun scripts/build.ts --minify", + "test": "bun run build && bun scripts/verify-theme.ts", + "compose:up": "docker compose -f compose.dev.yaml up -d", + "compose:down": "docker compose -f compose.dev.yaml down", + "compose:logs": "docker compose -f compose.dev.yaml logs -f server", + "docker:test": "bun run build && docker compose -f compose.dev.yaml up -d && bun scripts/verify-docker.ts" + }, + "devDependencies": { + "@types/bun": "latest", + "lightningcss": "^1.29.2", + "polished": "^4.3.1" + } +} diff --git a/scripts/build.ts b/scripts/build.ts new file mode 100644 index 0000000..be21fa5 --- /dev/null +++ b/scripts/build.ts @@ -0,0 +1,54 @@ +import { mkdir, writeFile } from "node:fs/promises"; +import { watch } from "node:fs"; +import { dirname, join } from "node:path"; +import { transform } from "lightningcss"; +import { stylesheet } from "../styles/index"; + +const root = process.cwd(); +const minify = process.argv.includes("--minify"); +const watchMode = process.argv.includes("--watch"); +const outputs = [ + join(root, "dist", "theme-gitea-auto.css"), + join(root, ".gitea", "custom", "public", "assets", "css", "theme-gitea-auto.css"), +]; + +async function build() { + const result = transform({ + filename: "theme-gitea-auto.css", + code: Buffer.from(stylesheet), + minify, + targets: { + chrome: 108 << 16, + firefox: 108 << 16, + safari: 16 << 16, + }, + }); + + await Promise.all( + outputs.map(async (output) => { + await mkdir(dirname(output), { recursive: true }); + await writeFile(output, result.code); + }), + ); + + console.log(`built ${outputs.length} css outputs`); +} + +await build(); + +if (watchMode) { + console.log("watching styles, themes, and src"); + const watchedDirs = [join(root, "styles"), join(root, "themes"), join(root, "src")]; + const watchers = watchedDirs.map((dir) => + watch(dir, { recursive: true }, async (_eventType, filename) => { + if (filename?.endsWith(".ts")) { + await build(); + } + }), + ); + + process.on("SIGINT", () => { + watchers.forEach((watcher) => watcher.close()); + process.exit(0); + }); +} diff --git a/scripts/preview.ts b/scripts/preview.ts new file mode 100644 index 0000000..7bb261b --- /dev/null +++ b/scripts/preview.ts @@ -0,0 +1,42 @@ +const server = Bun.serve({ + port: Number(process.env.PORT ?? 4173), + async fetch(request) { + const url = new URL(request.url); + + if (url.pathname === "/" || url.pathname === "/index.html") { + return new Response( + ` + + + + + + Gitea Theme Preview + + +
+

Gitea Theme Preview

+

用于快速检查 token、primitive、component 和 page 覆盖效果。

+ + +
+ +
+
+ +`, + { headers: { "content-type": "text/html; charset=utf-8" } }, + ); + } + + if (url.pathname === "/theme-gitea-auto.css") { + return new Response(Bun.file("dist/theme-gitea-auto.css"), { + headers: { "content-type": "text/css; charset=utf-8" }, + }); + } + + return new Response("Not Found", { status: 404 }); + }, +}); + +console.log(`preview http://localhost:${server.port}`); diff --git a/scripts/verify-docker.ts b/scripts/verify-docker.ts new file mode 100644 index 0000000..c820dea --- /dev/null +++ b/scripts/verify-docker.ts @@ -0,0 +1,33 @@ +const baseUrl = process.env.GITEA_PREVIEW_URL ?? "http://localhost:3000"; + +async function waitFor(url: string) { + const deadline = Date.now() + 60_000; + let lastError: unknown; + + while (Date.now() < deadline) { + try { + const response = await fetch(url); + if (response.ok) { + return response; + } + + lastError = new Error(`${url} returned ${response.status}`); + } catch (error) { + lastError = error; + } + + await Bun.sleep(1000); + } + + throw lastError; +} + +const home = await waitFor(baseUrl); +const theme = await waitFor(`${baseUrl}/assets/css/theme-gitea-auto.css`); +const css = await theme.text(); + +if (!css.includes("--gt-color-accent")) { + throw new Error("theme css is reachable but does not contain generated tokens"); +} + +console.log(`docker preview verified: ${home.status} ${baseUrl}`); diff --git a/scripts/verify-theme.ts b/scripts/verify-theme.ts new file mode 100644 index 0000000..da94e49 --- /dev/null +++ b/scripts/verify-theme.ts @@ -0,0 +1,21 @@ +import { existsSync } from "node:fs"; +import { stat } from "node:fs/promises"; +import { join } from "node:path"; + +const requiredOutputs = [ + join(process.cwd(), "dist", "theme-gitea-auto.css"), + join(process.cwd(), ".gitea", "custom", "public", "assets", "css", "theme-gitea-auto.css"), +]; + +for (const output of requiredOutputs) { + if (!existsSync(output)) { + throw new Error(`missing output: ${output}`); + } + + const file = await stat(output); + if (file.size === 0) { + throw new Error(`empty output: ${output}`); + } +} + +console.log("theme outputs verified"); diff --git a/src/css.ts b/src/css.ts new file mode 100644 index 0000000..52d2f02 --- /dev/null +++ b/src/css.ts @@ -0,0 +1,17 @@ +import type { ThemeScale, ThemeTokens } from "./theme-types"; + +const toKebab = (value: string) => + value.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`); + +const scaleToVars = (group: string, scale: ThemeScale) => + Object.entries(scale) + .map(([key, value]) => ` --gt-${group}-${toKebab(key)}: ${value};`) + .join("\n"); + +export const themeToRootVars = (theme: ThemeTokens) => { + const groups = Object.entries(theme) + .filter(([key]) => key !== "name") + .map(([group, scale]) => scaleToVars(group, scale as ThemeScale)); + + return `:root,\n.theme-${theme.name} {\n${groups.join("\n")}\n}`; +}; diff --git a/src/theme-types.ts b/src/theme-types.ts new file mode 100644 index 0000000..315c5d1 --- /dev/null +++ b/src/theme-types.ts @@ -0,0 +1,11 @@ +export type ThemeScale = Record; + +export interface ThemeTokens { + name: string; + color: ThemeScale; + radius: ThemeScale; + shadow: ThemeScale; + font: ThemeScale; + space: ThemeScale; + motion: ThemeScale; +} diff --git a/styles/components/index.ts b/styles/components/index.ts new file mode 100644 index 0000000..dfebc48 --- /dev/null +++ b/styles/components/index.ts @@ -0,0 +1,26 @@ +export const components = ` +.following.bar.light { + background: color-mix(in srgb, var(--gt-color-surface) 92%, transparent); + border-bottom: 1px solid var(--gt-color-border); + backdrop-filter: blur(14px); +} + +.repository .header-wrapper, +.repository.file.list #repo-files-table { + border-radius: var(--gt-radius-lg); + border: 1px solid var(--gt-color-border); + box-shadow: var(--gt-shadow-sm); +} + +.feeds .news, +.dashboard.feeds .news { + border-radius: var(--gt-radius-lg); + background: var(--gt-color-surface); +} + +.user.signin .ui.form, +.user.signup .ui.form { + border-radius: var(--gt-radius-lg); + box-shadow: var(--gt-shadow-md); +} +`; diff --git a/styles/index.ts b/styles/index.ts new file mode 100644 index 0000000..cc1af18 --- /dev/null +++ b/styles/index.ts @@ -0,0 +1,6 @@ +import { components } from "./components/index"; +import { pages } from "./pages/index"; +import { primitives } from "./primitives/index"; +import { tokens } from "./tokens/index"; + +export const stylesheet = [tokens, primitives, components, pages].join("\n\n"); diff --git a/styles/pages/index.ts b/styles/pages/index.ts new file mode 100644 index 0000000..de87299 --- /dev/null +++ b/styles/pages/index.ts @@ -0,0 +1,22 @@ +export const pages = ` +.page-content.dashboard { + padding-top: var(--gt-space-lg); +} + +.page-content.explore { + padding-top: var(--gt-space-xl); +} + +.user.signin .page-content, +.user.signup .page-content { + min-height: calc(100vh - 8rem); + display: grid; + place-items: center; +} + +.organization.new.org .page-content, +.organization.new.team .page-content { + max-width: 72rem; + margin-inline: auto; +} +`; diff --git a/styles/primitives/index.ts b/styles/primitives/index.ts new file mode 100644 index 0000000..abca5a3 --- /dev/null +++ b/styles/primitives/index.ts @@ -0,0 +1,51 @@ +export const primitives = ` +body { + color: var(--gt-color-text); + background: + radial-gradient(circle at 12% 8%, rgb(29 111 95 / 0.12), transparent 28rem), + linear-gradient(180deg, var(--gt-color-canvas), #efe6d4); + font-family: var(--gt-font-sans); +} + +a { + color: var(--gt-color-accent); +} + +.ui.button, +button.ui.button { + border-radius: var(--gt-radius-md); + border-color: var(--gt-color-border); + box-shadow: var(--gt-shadow-sm); + transition: + background-color var(--gt-motion-fast) ease, + border-color var(--gt-motion-fast) ease, + box-shadow var(--gt-motion-fast) ease; +} + +.ui.primary.button, +.ui.green.button { + background: var(--gt-color-accent); + color: #ffffff; +} + +.ui.primary.button:hover, +.ui.green.button:hover { + background: var(--gt-color-accent-hover); +} + +.ui.input > input, +.ui.form input:not([type]), +.ui.form input[type="text"], +.ui.form input[type="password"], +.ui.form input[type="email"], +.ui.form textarea { + border-radius: var(--gt-radius-md); + border-color: var(--gt-color-border); + background: var(--gt-color-surface-raised); +} + +.ui.dropdown, +.tippy-box { + border-radius: var(--gt-radius-md); +} +`; diff --git a/styles/tokens/index.ts b/styles/tokens/index.ts new file mode 100644 index 0000000..d7da1eb --- /dev/null +++ b/styles/tokens/index.ts @@ -0,0 +1,4 @@ +import { themeToRootVars } from "../../src/css"; +import { lightTheme } from "../../themes/light"; + +export const tokens = themeToRootVars(lightTheme); diff --git a/templates/README.md b/templates/README.md new file mode 100644 index 0000000..e69de29 diff --git a/templates/base/footer_content.tmpl b/templates/base/footer_content.tmpl new file mode 100644 index 0000000..c8d561e --- /dev/null +++ b/templates/base/footer_content.tmpl @@ -0,0 +1 @@ +{{/* Intentionally empty: hide Gitea's default page footer. */}} diff --git a/themes/light.ts b/themes/light.ts new file mode 100644 index 0000000..31183aa --- /dev/null +++ b/themes/light.ts @@ -0,0 +1,46 @@ +import type { ThemeTokens } from "../src/theme-types"; + +export const lightTheme: ThemeTokens = { + name: "gitea-auto", + color: { + canvas: "#f7f3ea", + surface: "#fffaf0", + surfaceRaised: "#ffffff", + border: "#d8cdb8", + text: "#1f2933", + textMuted: "#667085", + accent: "#1d6f5f", + accentHover: "#15574b", + danger: "#b42318", + warning: "#b54708", + success: "#027a48", + }, + radius: { + sm: "6px", + md: "10px", + lg: "16px", + pill: "999px", + }, + shadow: { + sm: "0 1px 2px rgb(31 41 51 / 0.08)", + md: "0 12px 30px rgb(31 41 51 / 0.12)", + }, + font: { + sans: "\"IBM Plex Sans\", \"Noto Sans SC\", sans-serif", + mono: "\"IBM Plex Mono\", \"Noto Sans Mono\", monospace", + sizeSm: "0.875rem", + sizeBase: "1rem", + sizeLg: "1.125rem", + }, + space: { + xs: "0.25rem", + sm: "0.5rem", + md: "1rem", + lg: "1.5rem", + xl: "2rem", + }, + motion: { + fast: "120ms", + normal: "180ms", + }, +}; diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..3c69c31 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "Bundler", + "types": ["bun"], + "strict": true, + "noEmit": true, + "skipLibCheck": true + }, + "include": ["scripts/**/*.ts", "src/**/*.ts", "styles/**/*.ts", "themes/**/*.ts"] +}