diff --git a/src/core/index.ts b/src/core/index.ts new file mode 100644 index 0000000..796bf88 --- /dev/null +++ b/src/core/index.ts @@ -0,0 +1 @@ +export { createTheme } from "./theme"; diff --git a/src/core/theme.ts b/src/core/theme.ts new file mode 100644 index 0000000..384597d --- /dev/null +++ b/src/core/theme.ts @@ -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>; + +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", + }); +} diff --git a/src/types.ts b/src/core/types.ts similarity index 100% rename from src/types.ts rename to src/core/types.ts diff --git a/src/vite.ts b/src/core/vite.ts similarity index 85% rename from src/vite.ts rename to src/core/vite.ts index 7b27ec7..893285a 100644 --- a/src/vite.ts +++ b/src/core/vite.ts @@ -3,6 +3,14 @@ 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, @@ -25,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}`); @@ -43,6 +51,10 @@ export function themeInput( const prefix = "theme-github-"; +/** + * 生成主题文件 + * @important vite.rollupOptions.output 配置 `assetFileNames: "[name].[ext]"` + */ export function themePlugin(): Plugin { return { name: "themePlugin", diff --git a/src/functions/index.ts b/src/functions/index.ts new file mode 100644 index 0000000..ba14d2b --- /dev/null +++ b/src/functions/index.ts @@ -0,0 +1 @@ +export { scaleColorLight } from "./scss"; diff --git a/src/functions/scss.ts b/src/functions/scss.ts new file mode 100644 index 0000000..1501751 --- /dev/null +++ b/src/functions/scss.ts @@ -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); +} diff --git a/src/index.ts b/src/index.ts index d9657f0..b7b8fe6 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +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"; diff --git a/src/theme.ts b/src/theme.ts deleted file mode 100644 index 5b2a7c3..0000000 --- a/src/theme.ts +++ /dev/null @@ -1,35 +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}`); - } -} - -const otherVars = { - border: { - radius: null, - }, -}; -const otherThemeVars = createGlobalThemeContract(otherVars, varMapper); -export const themeVars = createGlobalThemeContract(vars, varMapper); -export type Theme = WithOptionalLayer>; -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", - }); -} diff --git a/src/vars/color.ts b/src/types/color.ts similarity index 100% rename from src/vars/color.ts rename to src/types/color.ts diff --git a/src/color.ts b/src/types/index.ts similarity index 69% rename from src/color.ts rename to src/types/index.ts index d17f70b..646880f 100644 --- a/src/color.ts +++ b/src/types/index.ts @@ -1,5 +1,5 @@ -import type { MapLeafNodes } from "src/types"; -import { color } from "src/vars"; +import type { MapLeafNodes } from "src/core/types"; +import * as color from "./color"; export type Primary = MapLeafNodes; export type Secondary = MapLeafNodes; diff --git a/src/types/vars.ts b/src/types/vars.ts new file mode 100644 index 0000000..278cd90 --- /dev/null +++ b/src/types/vars.ts @@ -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); diff --git a/src/vars/index.ts b/src/vars/index.ts deleted file mode 100644 index 20c502d..0000000 --- a/src/vars/index.ts +++ /dev/null @@ -1,21 +0,0 @@ -import * as color from "src/vars/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; -} - -export const vars = { - /** 用于标识当前是否为暗色主题: `"true"` 暗色 `"false"` 亮色 */ - isDarkTheme: "is-dark-theme", - color: { - blue: null, - primary: color.primary, - }, -}; - -export { color }; diff --git a/styles/test.tsx b/styles/test.tsx index d74e8df..ecb64e2 100644 --- a/styles/test.tsx +++ b/styles/test.tsx @@ -1,15 +1,22 @@ -import { mix } from "polished"; import { css, themeVars } from "src"; +import { scaleColorLight } from "src/functions"; + +const red = "#cc4848"; export const setting_global = css` + @use "sass:color"; .lines-num span:after { color: ${themeVars.color.primary.hover}; } .ui.cards > .card, .ui.card { > .extra a:not(.ui):hover { - color: ${mix(0.1, "#fff", "#cc4848")}; - background-color: scale-color(#cc4848, $lightness: 10%); + color: ${scaleColorLight(red, 10)}; + background-color: color.scale(#cc4848, $lightness: 10%); + } + .text { + color: ${scaleColorLight(red, -20)}; + background-color: color.scale(#cc4848, $lightness: -20%); } } `; diff --git a/themes/dark.css.ts b/themes/dark.css.ts index 2043851..2c01ef1 100644 --- a/themes/dark.css.ts +++ b/themes/dark.css.ts @@ -1,4 +1,4 @@ -import { color } from "src"; +import type { Primary } from "src/types"; import { defineTheme, themeVars } from "src"; const dark = { @@ -33,7 +33,7 @@ const alpha = { num90: "#3683c0e1", }; -const primary: color.Primary = { +const primary: Primary = { self: themeVars.color.blue, contrast: "#fff", dark, diff --git a/vite.config.ts b/vite.config.ts index ec9a9ea..299a038 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -6,7 +6,7 @@ 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);