Compare commits

...

4 Commits

Author SHA1 Message Date
lutinglt
ac17e45069 CI 构建主题 2025-06-24 20:50:12 +08:00
lutinglt
f65338b227 添加版本脚本 2025-06-24 20:48:27 +08:00
lutinglt
711e01b66c src 结构调整, 添加 functions 2025-06-24 20:31:12 +08:00
lutinglt
9a070c5726 添加 sass 预处理 2025-06-24 13:52:03 +08:00
23 changed files with 366 additions and 177 deletions

View File

@@ -11,13 +11,14 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: denoland/setup-deno@v2
with:
deno-version: v2.x
- name: Build theme
run: |
npm install
npm run build
- name: Create release
run: |
export TZ=Asia/Shanghai
TAG="v$(deno run -q version).$(date +%y%m%d%H%M)"
TAG="v$(npm run -s version).$(date +%y%m%d%H%M)"
gh release create "$TAG" dist/* --notes-file .github/release.md --draft -t $TAG
env:
GH_TOKEN: ${{ github.token }}

2
.gitignore vendored
View File

@@ -1,3 +1,3 @@
dist
node_modules
package-lock.json
package-lock.json

View File

@@ -8,7 +8,8 @@
"build": "tsc -b && vite build",
"lint": "eslint .",
"format": "prettier --write .",
"commit": "npm run lint && npm run format && npm run build"
"commit": "npm run lint && npm run format && npm run build",
"version": "node scripts/version.cjs"
},
"devDependencies": {
"@babel/preset-react": "^7.27.1",
@@ -30,6 +31,7 @@
"eslint-plugin-react-refresh": "^0.4.20",
"globals": "^16.2.0",
"lightningcss": "^1.30.1",
"polished": "^4.3.1",
"prettier": "3.5.3",
"prettier-plugin-organize-imports": "^4.1.0",
"react": "^19.1.0",

8
scripts/version.cjs Normal file
View File

@@ -0,0 +1,8 @@
const fs = require('fs');
const path = require('path');
const pkgPath = path.join(__dirname, '..', 'package.json');
const pkg = JSON.parse(fs.readFileSync(pkgPath));
const version = pkg.version;
console.log(version);

View File

@@ -1,5 +0,0 @@
import type { MapLeafNodes } from "src/types";
import { color } from "src/vars";
export type Primary = MapLeafNodes<typeof color.primary, string>;
export type Secondary = MapLeafNodes<typeof color.secondary, string>;

1
src/core/index.ts Normal file
View File

@@ -0,0 +1 @@
export { createTheme } from "./theme";

48
src/core/theme.ts Normal file
View File

@@ -0,0 +1,48 @@
import { createGlobalTheme, globalStyle } from "@vanilla-extract/css";
import { otherThemeVars, themeVars } from "src/types/vars";
import type { MapLeafNodes, WithOptionalLayer } from "./types";
type Theme = WithOptionalLayer<MapLeafNodes<typeof themeVars, string>>;
function stringToBoolean(str: string, name: string): boolean {
try {
return JSON.parse(str);
} catch (error) {
console.error(error);
throw new Error(`Invalid boolean value(${name}): ${str}`);
}
}
/** 定义主题, 用于生成颜色主题
* @example
* 文件名: `color-dark.css.ts`
* import type { Primary } from "src/types";
* import { defineTheme, themeVars } from "src";
*
* const primary: Primary = {
* self: "#000000",
* contrast: themeVars.color.white,
* ...
* }
*
* export default defineTheme({
* isDarkTheme: "true",
* color: {
* primary,
* ...
* }
* })
*/
export const defineTheme = (theme: Theme) => theme;
export function createTheme(theme: Theme): void {
createGlobalTheme(":root", themeVars, theme);
createGlobalTheme(":root", otherThemeVars, {
border: {
radius: "6px",
},
});
globalStyle(":root", {
accentColor: themeVars.color.blue,
colorScheme: stringToBoolean(theme.isDarkTheme, "isDarkTheme") ? "dark" : "light",
});
}

View File

@@ -1,14 +1,25 @@
import fs from "fs";
import path from "path";
import crypto from "node:crypto";
import fs from "node:fs";
import path from "node:path";
import type { Plugin } from "vite";
/**
*
* @param outDir vite outDir ,
* @param themeDir
* @param devTheme ,
* @param mode , dev `vite build --mode dev`
* @returns vite.rollupOptions.input
*/
export function themeInput(
outDir: string,
themeDir: string,
devTheme: string,
mode: string
): { [key: string]: string } {
const tmpDir = `${outDir}/tmp`; // 输出目录下的临时目录
const hash = crypto.randomBytes(6).toString("hex");
const tmpDir = `${outDir}/tmp-${hash}`; // 输出目录下的临时目录
fs.mkdirSync(tmpDir, { recursive: true });
const input: { [key: string]: string } = {};
@@ -22,7 +33,7 @@ export function themeInput(
if (mode === "dev" && fileName !== devTheme) continue;
// 创建颜色主题的 css.ts 文件, vanilla-extract 需要这个文件后缀名并生成 css
const tmpCssTs = path.join(tmpDir, `${fileName}.css.ts`);
const createImport = `import { createTheme } from "src/theme";`;
const createImport = `import { createTheme } from "src/core";`;
const themeImport = `import theme from "themes/${fileName}";`;
const createFn = `createTheme(theme);`;
fs.writeFileSync(tmpCssTs, `${createImport}\n${themeImport}\n${createFn}`);
@@ -40,6 +51,10 @@ export function themeInput(
const prefix = "theme-github-";
/**
*
* @important vite.rollupOptions.output `assetFileNames: "[name].[ext]"`
*/
export function themePlugin(): Plugin {
return {
name: "themePlugin",

1
src/functions/index.ts Normal file
View File

@@ -0,0 +1 @@
export { scaleColorLight } from "./scss";

27
src/functions/scss.ts Normal file
View File

@@ -0,0 +1,27 @@
import { hsl, parseToHsl } from "polished";
/**
* 改变颜色的亮度, 等同于 sass 中的 `color.scale` 函数
* @param color 颜色值
* @param lightnessScale 亮度变化比例,负数表示变暗,正数表示变亮
* @returns 新的颜色值
* @example
* const newColor = scaleColorLight("#ff0000", 20); // 变亮
* const newColor = scaleColorLight("#ff0000", -20); // 变暗
* 等同于 sass `@use "sass:color"`;
* color: color.scale(#ff0000, $lightness: 20%)
* color: color.scale(#ff0000, $lightness: -20%)
*/
export function scaleColorLight(color: string, lightness: number) {
const hslColor = parseToHsl(color);
let newLightness;
if (lightness < 0) {
newLightness = hslColor.lightness * (1 + lightness / 100); // 变暗
} else {
newLightness = hslColor.lightness + (1 - hslColor.lightness) * (lightness / 100); // 变亮
}
newLightness = Math.min(1, Math.max(0, newLightness)); // 确保亮度值在 0 到 1 之间
return hsl(hslColor.hue, hslColor.saturation, newLightness);
}

View File

@@ -1,2 +1,3 @@
export * as color from "src/color";
export { defineTheme, themeVars } from "src/theme";
export { css } from "@linaria/core";
export { defineTheme } from "./core/theme";
export { themeVars } from "./types/vars";

View File

@@ -1,24 +0,0 @@
import { createGlobalTheme, createGlobalThemeContract, globalStyle } from "@vanilla-extract/css";
import type { MapLeafNodes, WithOptionalLayer } from "src/types";
import { varMapper, vars } from "src/vars";
function stringToBoolean(str: string, name: string): boolean {
try {
return JSON.parse(str);
} catch (error) {
console.error(error);
throw new Error(`Invalid boolean value(${name}): ${str}`);
}
}
export const themeVars = createGlobalThemeContract(vars, varMapper);
export type Theme = WithOptionalLayer<MapLeafNodes<typeof themeVars, string>>;
export const defineTheme = (theme: Theme) => theme;
export function createTheme(theme: Theme): void {
createGlobalTheme(":root", themeVars, theme);
globalStyle(":root", {
accentColor: themeVars.color.blue,
colorScheme: stringToBoolean(theme.isDarkTheme, "isDarkTheme") ? "dark" : "light",
});
}

140
src/types/color.ts Normal file
View File

@@ -0,0 +1,140 @@
const num = {
num1: null,
num2: null,
num3: null,
num4: null,
num5: null,
num6: null,
num7: null,
};
const alpha = {
num10: null,
num20: null,
num30: null,
num40: null,
num50: null,
num60: null,
num70: null,
num80: null,
num90: null,
};
export const primary = {
self: null,
contrast: null,
dark: num,
light: num,
alpha: alpha,
hover: null,
active: null,
};
export const secondary = {
self: null,
dark: {
num8: null,
num9: null,
num10: null,
num11: null,
num12: null,
num13: null,
...num,
},
light: {
num1: null,
num2: null,
num3: null,
num4: null,
},
alpha: alpha,
};
const baseColor = {
self: null,
light: null,
dark: {
num1: null,
num2: null,
},
};
export const self = {
red: baseColor,
orange: baseColor,
yellow: baseColor,
olive: baseColor,
green: baseColor,
teal: baseColor,
blue: baseColor,
violet: baseColor,
purple: baseColor,
pink: baseColor,
brown: baseColor,
black: baseColor,
grey: {
self: null,
light: null,
},
gold: null,
white: null,
};
const ansiColor = {
black: null,
red: null,
green: null,
yellow: null,
blue: null,
magenta: null,
cyan: null,
white: null,
};
export const ansi = {
bright: ansiColor,
...ansiColor,
};
export const console = {
fg: {
self: null,
subtle: null,
},
bg: null,
border: null,
active: {
bg: null,
},
hover: {
bg: null,
},
menu: {
bg: null,
border: null,
},
};
const row = {
bg: null,
border: null,
};
const line = {
linenum: {
bg: null,
},
row: row,
word: {
bg: null,
},
};
export const diff = {
added: line,
moved: {
row: row,
},
removed: line,
inactive: null,
};

6
src/types/index.ts Normal file
View File

@@ -0,0 +1,6 @@
import type { MapLeafNodes } from "src/core/types";
import * as color from "./color";
export type Primary = MapLeafNodes<typeof color.primary, string>;
export type Secondary = MapLeafNodes<typeof color.secondary, string>;
export type Self = MapLeafNodes<typeof color.self, string>;

29
src/types/vars.ts Normal file
View File

@@ -0,0 +1,29 @@
import { createGlobalThemeContract } from "@vanilla-extract/css";
import * as color from "./color";
export function varMapper(value: string | null, path: string[]) {
if (value === null) {
path = path.filter(item => item !== "self");
path = path.map(item => item.replace(/^num/, ""));
return path.join("-");
}
return value;
}
const vars = {
/** 用于标识当前是否为暗色主题: `"true"` 暗色 `"false"` 亮色 */
isDarkTheme: "is-dark-theme",
color: {
blue: null,
primary: color.primary,
},
};
const otherVars = {
border: {
radius: null,
},
};
export const themeVars = createGlobalThemeContract(vars, varMapper);
export const otherThemeVars = createGlobalThemeContract(otherVars, varMapper);

View File

@@ -1,69 +0,0 @@
const num = {
1: null,
2: null,
3: null,
4: null,
5: null,
6: null,
7: null,
};
const alpha = {
10: null,
20: null,
30: null,
40: null,
50: null,
60: null,
70: null,
80: null,
90: null,
};
export const primary = {
self: null,
contrast: null,
dark: num,
light: num,
alpha: alpha,
hover: null,
active: null,
};
export const secondary = {
self: null,
dark: {
8: null,
9: null,
10: null,
11: null,
12: null,
13: null,
...num,
},
light: {
1: null,
2: null,
3: null,
4: null,
},
alpha: alpha,
};
export const color = {
red: null,
orange: null,
yellow: null,
olive: null,
green: null,
teal: null,
blue: null,
violet: null,
purple: null,
pink: null,
brown: null,
black: null,
grey: null,
gold: null,
white: null,
};

View File

@@ -1,20 +0,0 @@
import * as color from "src/vars/color";
export function varMapper(value: string | null, path: string[]) {
if (value === null) {
if (path.at(-1) === "self") return path.slice(0, -1).join("-");
return path.join("-");
}
return value;
}
export const vars = {
/** 用于标识当前是否为暗色主题: `"true"` 暗色 `"false"` 亮色 */
isDarkTheme: "is-dark-theme",
color: {
blue: null,
primary: color.primary,
},
};
export { color };

View File

@@ -1 +1,2 @@
import "styles/test";
import "styles/t1";

20
styles/t1.tsx Normal file
View File

@@ -0,0 +1,20 @@
import { css, themeVars } from "src";
export const setting_global = css`
.user-main-content,
.repo-setting-content,
.user-setting-content,
.org-setting-content,
.admin-setting-content {
.ui.right {
.ui.primary.button.tiny {
color: #fff;
background-color: #238636;
&:hover {
background-color: #29903b;
border-color: ${themeVars.color.primary.light.num1};
}
}
}
}
`;

View File

@@ -1,23 +1,22 @@
import { css } from "@linaria/core";
import { themeVars } from "src/theme";
import { css, themeVars } from "src";
import { scaleColorLight } from "src/functions";
const red = "#cc4848";
export const setting_global = css`
:global() {
.user-main-content,
.repo-setting-content,
.user-setting-content,
.org-setting-content,
.admin-setting-content {
.ui.right {
.ui.primary.button.tiny {
color: #fff;
background-color: #238636;
&:hover {
background-color: #29903b;
border-color: ${themeVars.color.primary.light[1]};
}
}
}
@use "sass:color";
.lines-num span:after {
color: ${themeVars.color.primary.hover};
}
.ui.cards > .card,
.ui.card {
> .extra a:not(.ui):hover {
color: ${scaleColorLight(red, 10)};
background-color: color.scale(#cc4848, $lightness: 10%);
}
.text {
color: ${scaleColorLight(red, -20)};
background-color: color.scale(#cc4848, $lightness: -20%);
}
}
`;

View File

@@ -1,46 +1,46 @@
import type { color } from "src";
import type { Primary } from "src/types";
import { defineTheme, themeVars } from "src";
const dark = {
1: "#739cb3",
2: "#40aaff",
3: "#92b4c4",
4: "#a1bbcd",
5: "#cfddc1",
6: "#e7eee0",
7: "#f8faf6",
num1: "#739cb3",
num2: "#40aaff",
num3: "#92b4c4",
num4: "#a1bbcd",
num5: "#cfddc1",
num6: "#e7eee0",
num7: "#f8faf6",
};
const light = {
1: themeVars.color.blue,
2: "#437aad",
3: "#415b8b",
4: "#25425a",
5: "#223546",
6: "#131923",
7: "#06090b",
num1: themeVars.color.primary.self,
num2: "#437aad",
num3: "#415b8b",
num4: "#25425a",
num5: "#223546",
num6: "#131923",
num7: "#06090b",
};
const alpha = {
10: "#3683c019",
20: "#3683c033",
30: "#3683c04b",
40: "#3683c066",
50: "#3683c080",
60: "#3683c099",
70: "#3683c0b3",
80: "#3683c0cc",
90: "#3683c0e1",
num10: "#3683c019",
num20: "#3683c033",
num30: "#3683c04b",
num40: "#3683c066",
num50: "#3683c080",
num60: "#3683c099",
num70: "#3683c0b3",
num80: "#3683c0cc",
num90: "#3683c0e1",
};
const primary: color.Primary = {
const primary: Primary = {
self: themeVars.color.blue,
contrast: "#fff",
dark,
light,
alpha,
hover: light[1],
active: light[2],
hover: light.num1,
active: light.num2,
};
export default defineTheme({

View File

@@ -2,9 +2,13 @@ import { vanillaExtractPlugin } from "@vanilla-extract/vite-plugin";
import react from "@vitejs/plugin-react";
import linaria from "@wyw-in-js/vite";
import { Features } from "lightningcss";
import path from "path";
import { createRequire } from "node:module";
import path from "node:path";
import * as sass from "sass-embedded";
import { defineConfig } from "vite";
import { themeInput, themePlugin } from "./src/vite";
import { themeInput, themePlugin } from "./src/core/vite";
const require = createRequire(import.meta.url);
const devTheme = "dark"; // 开发模式仅打包单个颜色主题
const outDir = "dist"; // 输出目录
@@ -33,6 +37,10 @@ export default defineConfig(({ mode }) => {
babelOptions: {
presets: ["@babel/preset-typescript", "@babel/preset-react"],
},
preprocessor: (_selector, cssText) => sass.compileString(cssText).css, // 默认为全局样式并使用 sass-embedded 预处理 css
tagResolver: (source, tag) =>
// 识别从 src 导出的 css 标签,使用 @linaria/core/processors/css 处理
source === "src" && tag === "css" ? require.resolve("@linaria/core/processors/css") : null,
}),
react(),
vanillaExtractPlugin(),