@zl-ui-kit/theme 0.25.4 → 0.25.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +62 -62
- package/dist/index.js +18 -2
- package/package.json +1 -1
- package/src/generated/theme-blue.css +6 -0
- package/src/generated/theme-dark.css +6 -0
- package/src/generated/theme-red-dark.css +6 -0
- package/src/generated/theme-red-light.css +6 -0
- package/src/generated/themes.css +24 -0
- package/src/index.ts +12 -12
- package/src/runtime/api.ts +327 -327
package/README.md
CHANGED
|
@@ -1,62 +1,62 @@
|
|
|
1
|
-
# @zl-ui-kit/theme
|
|
2
|
-
|
|
3
|
-
> **完整使用文档**:[`@zl-ui-kit/components` 的 README(npm 页面)](https://www.npmjs.com/package/@zl-ui-kit/components) 即 UI 组件权威文档 ——
|
|
4
|
-
> 本包的 Hooks / 工具 / 主题 / 国际化用法都在其中,本文只保留本包速查与运行前提。
|
|
5
|
-
|
|
6
|
-
ZL UI 主题引擎(决策 D7):内置 5 套主题(light/dark/red-light/red-dark/blue),`body[data-theme]` 作用域 + `--zl-*`(业务层)/`--el-*`(Element Plus 层)变量体系,支持运行时自定义主题。
|
|
7
|
-
|
|
8
|
-
## 快速上手
|
|
9
|
-
|
|
10
|
-
**① 用默认主题:只引一个 CSS,不写任何主题代码。**
|
|
11
|
-
|
|
12
|
-
```ts
|
|
13
|
-
// main.ts —— 顺序不能倒:EP 样式在前、主题在后,否则被 EP 自带默认值压掉
|
|
14
|
-
import 'element-plus/dist/index.css'
|
|
15
|
-
import '@zl-ui-kit/theme/themes.css' // 5 套全量;只要默认一套可换成 '@zl-ui-kit/theme/light'
|
|
16
|
-
```
|
|
17
|
-
|
|
18
|
-
引入即得到内置 `light`:主色 `#3056D3`、hover `#6e89e0`、按下 `#2645a9`、控件圆角 `4px`;
|
|
19
|
-
业务组件(`--zl-*`)与 Element Plus 控件(`--el-*`)共用同一组变量。
|
|
20
|
-
|
|
21
|
-
**② 要覆盖默认主题:在宿主侧一个入口文件里于启动时调用。**
|
|
22
|
-
|
|
23
|
-
```ts
|
|
24
|
-
import { defineTheme, enableThemePersistence, setTheme, useTheme } from '@zl-ui-kit/theme'
|
|
25
|
-
|
|
26
|
-
setTheme('dark') // 切内置主题:light / dark / red-light / red-dark / blue
|
|
27
|
-
enableThemePersistence() // 可选:刷新后保持(storage: zl-theme)
|
|
28
|
-
|
|
29
|
-
// 整套自定义:以内置主题为基线,只声明要改的量,色阶自动派生
|
|
30
|
-
defineTheme({
|
|
31
|
-
name: 'brand',
|
|
32
|
-
base: 'light',
|
|
33
|
-
colors: { primary: '#722ED1' }, // → --zl-primary-1..10 与 --el-color-primary-light-*/dark-2
|
|
34
|
-
tokens: { 'radius-base': '2px' }, // → 同时写 --el-border-radius-base,EP 控件跟着变
|
|
35
|
-
apply: true,
|
|
36
|
-
})
|
|
37
|
-
|
|
38
|
-
// 组件内响应式读当前主题(theme 是只读 ref,切换仍走上面那个 setTheme)
|
|
39
|
-
const { theme } = useTheme()
|
|
40
|
-
```
|
|
41
|
-
|
|
42
|
-
页面里只引用语义变量(`var(--el-color-primary)` / `var(--zl-*)`),不要再写色值字面量,
|
|
43
|
-
也不要把主题色“回写”到另一个变量上 —— 取色单源才能避免组件一个色系、控件另一个色系。
|
|
44
|
-
|
|
45
|
-
**默认与定制的完整选型(L1 变量直覆 / L2 运行时 defineTheme / L3 构建时生成)、色阶契约与不生效排查见
|
|
46
|
-
`@zl-ui-kit/components` 的 README §10 主题定制**;可运行的实时版(含「当前实际生效值」读数)见 playground `/theme` 页。
|
|
47
|
-
|
|
48
|
-
## 入口
|
|
49
|
-
|
|
50
|
-
| 入口 | 内容 |
|
|
51
|
-
| --------------------------------------------------------------- | ---------------------------------------------------------------------- |
|
|
52
|
-
| `.` | 运行时 API(defineTheme/setTheme/useTheme/mix/generateElShades)+ 常量 |
|
|
53
|
-
| `/light` `/dark` `/red-light` `/red-dark` `/blue` `/themes.css` | 每主题/合集预编译 CSS(变量 + reset + overrides) |
|
|
54
|
-
| `/scss`(及 `/scss/*.scss`) | SCSS 工具(colorForEach/colorGradient/theme mixin/mapGet) |
|
|
55
|
-
| `/assets/*` | 主题背景图 |
|
|
56
|
-
|
|
57
|
-
## 本地调试
|
|
58
|
-
|
|
59
|
-
- 改 `_json/*.json`(主题 token)→ `pnpm --filter @zl-ui-kit/theme gen` → playground `/theme` 页刷新
|
|
60
|
-
- `pnpm build` = gen + tsup;themegen 产物在 `src/generated/`(勿手改)
|
|
61
|
-
|
|
62
|
-
详见 `@zl-ui-kit/components` README §11;发布见 `docs/发布流程.md`。
|
|
1
|
+
# @zl-ui-kit/theme
|
|
2
|
+
|
|
3
|
+
> **完整使用文档**:[`@zl-ui-kit/components` 的 README(npm 页面)](https://www.npmjs.com/package/@zl-ui-kit/components) 即 UI 组件权威文档 ——
|
|
4
|
+
> 本包的 Hooks / 工具 / 主题 / 国际化用法都在其中,本文只保留本包速查与运行前提。
|
|
5
|
+
|
|
6
|
+
ZL UI 主题引擎(决策 D7):内置 5 套主题(light/dark/red-light/red-dark/blue),`body[data-theme]` 作用域 + `--zl-*`(业务层)/`--el-*`(Element Plus 层)变量体系,支持运行时自定义主题。
|
|
7
|
+
|
|
8
|
+
## 快速上手
|
|
9
|
+
|
|
10
|
+
**① 用默认主题:只引一个 CSS,不写任何主题代码。**
|
|
11
|
+
|
|
12
|
+
```ts
|
|
13
|
+
// main.ts —— 顺序不能倒:EP 样式在前、主题在后,否则被 EP 自带默认值压掉
|
|
14
|
+
import 'element-plus/dist/index.css'
|
|
15
|
+
import '@zl-ui-kit/theme/themes.css' // 5 套全量;只要默认一套可换成 '@zl-ui-kit/theme/light'
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
引入即得到内置 `light`:主色 `#3056D3`、hover `#6e89e0`、按下 `#2645a9`、控件圆角 `4px`;
|
|
19
|
+
业务组件(`--zl-*`)与 Element Plus 控件(`--el-*`)共用同一组变量。
|
|
20
|
+
|
|
21
|
+
**② 要覆盖默认主题:在宿主侧一个入口文件里于启动时调用。**
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
import { defineTheme, enableThemePersistence, setTheme, useTheme } from '@zl-ui-kit/theme'
|
|
25
|
+
|
|
26
|
+
setTheme('dark') // 切内置主题:light / dark / red-light / red-dark / blue
|
|
27
|
+
enableThemePersistence() // 可选:刷新后保持(storage: zl-theme)
|
|
28
|
+
|
|
29
|
+
// 整套自定义:以内置主题为基线,只声明要改的量,色阶自动派生
|
|
30
|
+
defineTheme({
|
|
31
|
+
name: 'brand',
|
|
32
|
+
base: 'light',
|
|
33
|
+
colors: { primary: '#722ED1' }, // → --zl-primary-1..10 与 --el-color-primary-light-*/dark-2
|
|
34
|
+
tokens: { 'radius-base': '2px' }, // → 同时写 --el-border-radius-base,EP 控件跟着变
|
|
35
|
+
apply: true,
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
// 组件内响应式读当前主题(theme 是只读 ref,切换仍走上面那个 setTheme)
|
|
39
|
+
const { theme } = useTheme()
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
页面里只引用语义变量(`var(--el-color-primary)` / `var(--zl-*)`),不要再写色值字面量,
|
|
43
|
+
也不要把主题色“回写”到另一个变量上 —— 取色单源才能避免组件一个色系、控件另一个色系。
|
|
44
|
+
|
|
45
|
+
**默认与定制的完整选型(L1 变量直覆 / L2 运行时 defineTheme / L3 构建时生成)、色阶契约与不生效排查见
|
|
46
|
+
`@zl-ui-kit/components` 的 README §10 主题定制**;可运行的实时版(含「当前实际生效值」读数)见 playground `/theme` 页。
|
|
47
|
+
|
|
48
|
+
## 入口
|
|
49
|
+
|
|
50
|
+
| 入口 | 内容 |
|
|
51
|
+
| --------------------------------------------------------------- | ---------------------------------------------------------------------- |
|
|
52
|
+
| `.` | 运行时 API(defineTheme/setTheme/useTheme/mix/generateElShades)+ 常量 |
|
|
53
|
+
| `/light` `/dark` `/red-light` `/red-dark` `/blue` `/themes.css` | 每主题/合集预编译 CSS(变量 + reset + overrides) |
|
|
54
|
+
| `/scss`(及 `/scss/*.scss`) | SCSS 工具(colorForEach/colorGradient/theme mixin/mapGet) |
|
|
55
|
+
| `/assets/*` | 主题背景图 |
|
|
56
|
+
|
|
57
|
+
## 本地调试
|
|
58
|
+
|
|
59
|
+
- 改 `_json/*.json`(主题 token)→ `pnpm --filter @zl-ui-kit/theme gen` → playground `/theme` 页刷新
|
|
60
|
+
- `pnpm build` = gen + tsup;themegen 产物在 `src/generated/`(勿手改)
|
|
61
|
+
|
|
62
|
+
详见 `@zl-ui-kit/components` README §11;发布见 `docs/发布流程.md`。
|
package/dist/index.js
CHANGED
|
@@ -1253,15 +1253,31 @@ function buildCssVars(resolved, themeName, baseName = themeName) {
|
|
|
1253
1253
|
decls.push(`--el-border-radius-base: ${get("radius-base")};`);
|
|
1254
1254
|
decls.push(`--el-border-radius-small: ${get("radius-small")};`);
|
|
1255
1255
|
const isPlainBase = baseName === "light" || baseName === "dark";
|
|
1256
|
+
const isLightBase = baseName === "light";
|
|
1256
1257
|
decls.push(`--zl-menu-hover-bg: ${isPlainBase ? "var(--zl-fill-2)" : "rgba(255, 255, 255, 0.2)"};`);
|
|
1257
1258
|
decls.push(`--zl-menu-text-color: ${isPlainBase ? "var(--zl-text-1)" : "var(--zl-white)"};`);
|
|
1259
|
+
if (isLightBase) {
|
|
1260
|
+
decls.push("--zl-menu-active-bg: var(--zl-primary-1);");
|
|
1261
|
+
decls.push("--zl-menu-active-color: var(--zl-primary-6);");
|
|
1262
|
+
decls.push("--zl-menu-active-border-color: var(--zl-primary-6);");
|
|
1263
|
+
decls.push("--zl-menu-icon-color: var(--zl-text-3);");
|
|
1264
|
+
decls.push("--zl-menu-font-size: 14px;");
|
|
1265
|
+
decls.push("--zl-menu-item-height: 40px;");
|
|
1266
|
+
} else {
|
|
1267
|
+
decls.push("--zl-menu-active-bg: var(--zl-menu-hover-bg);");
|
|
1268
|
+
decls.push("--zl-menu-active-color: var(--zl-menu-text-color);");
|
|
1269
|
+
decls.push("--zl-menu-active-border-color: transparent;");
|
|
1270
|
+
decls.push("--zl-menu-icon-color: currentColor;");
|
|
1271
|
+
decls.push("--zl-menu-font-size: 12px;");
|
|
1272
|
+
decls.push("--zl-menu-item-height: 36px;");
|
|
1273
|
+
}
|
|
1258
1274
|
decls.push(`--zl-menu-trigger-arrow-color: ${isDark ? "#253044" : "#f6fafc"};`);
|
|
1259
1275
|
decls.push(`--zl-menu-trigger-bg: linear-gradient(135deg, var(--zl-modo-5), var(--zl-modo-7));`);
|
|
1260
1276
|
decls.push(
|
|
1261
|
-
`--zl-layout-vertical-aside-bg: linear-gradient(180deg, var(--zl-modo-7) 0%, var(--zl-modo-9) 100%);`
|
|
1277
|
+
isLightBase ? `--zl-layout-vertical-aside-bg: linear-gradient(180deg, var(--zl-primary-1) 0%, var(--zl-bg-1) 100%);` : `--zl-layout-vertical-aside-bg: linear-gradient(180deg, var(--zl-modo-7) 0%, var(--zl-modo-9) 100%);`
|
|
1262
1278
|
);
|
|
1263
1279
|
decls.push(
|
|
1264
|
-
`--zl-layout-horizontal-header-bg: linear-gradient(90deg, var(--zl-modo-7) 0%, var(--zl-modo-6) 60%, var(--zl-modo-5) 100%);`
|
|
1280
|
+
isLightBase ? `--zl-layout-horizontal-header-bg: var(--zl-bg-1);` : `--zl-layout-horizontal-header-bg: linear-gradient(90deg, var(--zl-modo-7) 0%, var(--zl-modo-6) 60%, var(--zl-modo-5) 100%);`
|
|
1265
1281
|
);
|
|
1266
1282
|
return decls.join("\n ");
|
|
1267
1283
|
}
|
package/package.json
CHANGED
|
@@ -336,6 +336,12 @@ body[data-theme='blue'] {
|
|
|
336
336
|
--el-border-radius-small: 2px;
|
|
337
337
|
--zl-menu-hover-bg: rgba(255, 255, 255, 0.2);
|
|
338
338
|
--zl-menu-text-color: var(--zl-white);
|
|
339
|
+
--zl-menu-active-bg: var(--zl-menu-hover-bg);
|
|
340
|
+
--zl-menu-active-color: var(--zl-menu-text-color);
|
|
341
|
+
--zl-menu-active-border-color: transparent;
|
|
342
|
+
--zl-menu-icon-color: currentColor;
|
|
343
|
+
--zl-menu-font-size: 12px;
|
|
344
|
+
--zl-menu-item-height: 36px;
|
|
339
345
|
--zl-menu-trigger-arrow-color: #306bca;
|
|
340
346
|
--zl-menu-trigger-bg: linear-gradient(135deg, var(--zl-modo-5), var(--zl-modo-7));
|
|
341
347
|
--zl-layout-vertical-aside-bg: linear-gradient(180deg, var(--zl-modo-7) 0%, var(--zl-modo-9) 100%);
|
|
@@ -336,6 +336,12 @@ body[data-theme='dark'] {
|
|
|
336
336
|
--el-border-radius-small: 2px;
|
|
337
337
|
--zl-menu-hover-bg: var(--zl-fill-2);
|
|
338
338
|
--zl-menu-text-color: var(--zl-text-1);
|
|
339
|
+
--zl-menu-active-bg: var(--zl-menu-hover-bg);
|
|
340
|
+
--zl-menu-active-color: var(--zl-menu-text-color);
|
|
341
|
+
--zl-menu-active-border-color: transparent;
|
|
342
|
+
--zl-menu-icon-color: currentColor;
|
|
343
|
+
--zl-menu-font-size: 12px;
|
|
344
|
+
--zl-menu-item-height: 36px;
|
|
339
345
|
--zl-menu-trigger-arrow-color: #253044;
|
|
340
346
|
--zl-menu-trigger-bg: linear-gradient(135deg, var(--zl-modo-5), var(--zl-modo-7));
|
|
341
347
|
--zl-layout-vertical-aside-bg: linear-gradient(180deg, var(--zl-modo-7) 0%, var(--zl-modo-9) 100%);
|
|
@@ -336,6 +336,12 @@ body[data-theme='red-dark'] {
|
|
|
336
336
|
--el-border-radius-small: 2px;
|
|
337
337
|
--zl-menu-hover-bg: rgba(255, 255, 255, 0.2);
|
|
338
338
|
--zl-menu-text-color: var(--zl-white);
|
|
339
|
+
--zl-menu-active-bg: var(--zl-menu-hover-bg);
|
|
340
|
+
--zl-menu-active-color: var(--zl-menu-text-color);
|
|
341
|
+
--zl-menu-active-border-color: transparent;
|
|
342
|
+
--zl-menu-icon-color: currentColor;
|
|
343
|
+
--zl-menu-font-size: 12px;
|
|
344
|
+
--zl-menu-item-height: 36px;
|
|
339
345
|
--zl-menu-trigger-arrow-color: #263247;
|
|
340
346
|
--zl-menu-trigger-bg: linear-gradient(135deg, var(--zl-modo-5), var(--zl-modo-7));
|
|
341
347
|
--zl-layout-vertical-aside-bg: linear-gradient(180deg, var(--zl-modo-7) 0%, var(--zl-modo-9) 100%);
|
|
@@ -336,6 +336,12 @@ body[data-theme='red-light'] {
|
|
|
336
336
|
--el-border-radius-small: 2px;
|
|
337
337
|
--zl-menu-hover-bg: rgba(255, 255, 255, 0.2);
|
|
338
338
|
--zl-menu-text-color: var(--zl-white);
|
|
339
|
+
--zl-menu-active-bg: var(--zl-menu-hover-bg);
|
|
340
|
+
--zl-menu-active-color: var(--zl-menu-text-color);
|
|
341
|
+
--zl-menu-active-border-color: transparent;
|
|
342
|
+
--zl-menu-icon-color: currentColor;
|
|
343
|
+
--zl-menu-font-size: 12px;
|
|
344
|
+
--zl-menu-item-height: 36px;
|
|
339
345
|
--zl-menu-trigger-arrow-color: #a51e1e;
|
|
340
346
|
--zl-menu-trigger-bg: linear-gradient(135deg, var(--zl-modo-5), var(--zl-modo-7));
|
|
341
347
|
--zl-layout-vertical-aside-bg: linear-gradient(180deg, var(--zl-modo-7) 0%, var(--zl-modo-9) 100%);
|
package/src/generated/themes.css
CHANGED
|
@@ -336,6 +336,12 @@ body[data-theme='blue'] {
|
|
|
336
336
|
--el-border-radius-small: 2px;
|
|
337
337
|
--zl-menu-hover-bg: rgba(255, 255, 255, 0.2);
|
|
338
338
|
--zl-menu-text-color: var(--zl-white);
|
|
339
|
+
--zl-menu-active-bg: var(--zl-menu-hover-bg);
|
|
340
|
+
--zl-menu-active-color: var(--zl-menu-text-color);
|
|
341
|
+
--zl-menu-active-border-color: transparent;
|
|
342
|
+
--zl-menu-icon-color: currentColor;
|
|
343
|
+
--zl-menu-font-size: 12px;
|
|
344
|
+
--zl-menu-item-height: 36px;
|
|
339
345
|
--zl-menu-trigger-arrow-color: #306bca;
|
|
340
346
|
--zl-menu-trigger-bg: linear-gradient(135deg, var(--zl-modo-5), var(--zl-modo-7));
|
|
341
347
|
--zl-layout-vertical-aside-bg: linear-gradient(180deg, var(--zl-modo-7) 0%, var(--zl-modo-9) 100%);
|
|
@@ -701,6 +707,12 @@ body[data-theme='dark'] {
|
|
|
701
707
|
--el-border-radius-small: 2px;
|
|
702
708
|
--zl-menu-hover-bg: var(--zl-fill-2);
|
|
703
709
|
--zl-menu-text-color: var(--zl-text-1);
|
|
710
|
+
--zl-menu-active-bg: var(--zl-menu-hover-bg);
|
|
711
|
+
--zl-menu-active-color: var(--zl-menu-text-color);
|
|
712
|
+
--zl-menu-active-border-color: transparent;
|
|
713
|
+
--zl-menu-icon-color: currentColor;
|
|
714
|
+
--zl-menu-font-size: 12px;
|
|
715
|
+
--zl-menu-item-height: 36px;
|
|
704
716
|
--zl-menu-trigger-arrow-color: #253044;
|
|
705
717
|
--zl-menu-trigger-bg: linear-gradient(135deg, var(--zl-modo-5), var(--zl-modo-7));
|
|
706
718
|
--zl-layout-vertical-aside-bg: linear-gradient(180deg, var(--zl-modo-7) 0%, var(--zl-modo-9) 100%);
|
|
@@ -1424,6 +1436,12 @@ body[data-theme='red-dark'] {
|
|
|
1424
1436
|
--el-border-radius-small: 2px;
|
|
1425
1437
|
--zl-menu-hover-bg: rgba(255, 255, 255, 0.2);
|
|
1426
1438
|
--zl-menu-text-color: var(--zl-white);
|
|
1439
|
+
--zl-menu-active-bg: var(--zl-menu-hover-bg);
|
|
1440
|
+
--zl-menu-active-color: var(--zl-menu-text-color);
|
|
1441
|
+
--zl-menu-active-border-color: transparent;
|
|
1442
|
+
--zl-menu-icon-color: currentColor;
|
|
1443
|
+
--zl-menu-font-size: 12px;
|
|
1444
|
+
--zl-menu-item-height: 36px;
|
|
1427
1445
|
--zl-menu-trigger-arrow-color: #263247;
|
|
1428
1446
|
--zl-menu-trigger-bg: linear-gradient(135deg, var(--zl-modo-5), var(--zl-modo-7));
|
|
1429
1447
|
--zl-layout-vertical-aside-bg: linear-gradient(180deg, var(--zl-modo-7) 0%, var(--zl-modo-9) 100%);
|
|
@@ -1776,6 +1794,12 @@ body[data-theme='red-light'] {
|
|
|
1776
1794
|
--el-border-radius-small: 2px;
|
|
1777
1795
|
--zl-menu-hover-bg: rgba(255, 255, 255, 0.2);
|
|
1778
1796
|
--zl-menu-text-color: var(--zl-white);
|
|
1797
|
+
--zl-menu-active-bg: var(--zl-menu-hover-bg);
|
|
1798
|
+
--zl-menu-active-color: var(--zl-menu-text-color);
|
|
1799
|
+
--zl-menu-active-border-color: transparent;
|
|
1800
|
+
--zl-menu-icon-color: currentColor;
|
|
1801
|
+
--zl-menu-font-size: 12px;
|
|
1802
|
+
--zl-menu-item-height: 36px;
|
|
1779
1803
|
--zl-menu-trigger-arrow-color: #a51e1e;
|
|
1780
1804
|
--zl-menu-trigger-bg: linear-gradient(135deg, var(--zl-modo-5), var(--zl-modo-7));
|
|
1781
1805
|
--zl-layout-vertical-aside-bg: linear-gradient(180deg, var(--zl-modo-7) 0%, var(--zl-modo-9) 100%);
|
package/src/index.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* ============================================================
|
|
3
|
-
* @zl-ui-kit/theme 入口(运行时 API + 常量)
|
|
4
|
-
* 主题 CSS 入口:@zl-ui-kit/theme/light|dark|red-light|red-dark|blue 或 @zl-ui-kit/theme/themes.css
|
|
5
|
-
* SCSS 工具入口:@zl-ui-kit/theme/scss
|
|
6
|
-
* 自定义主题:见 @zl-ui-kit/components 的 README §10 主题定制(L1 变量直覆 / L2 defineTheme / L3 构建时生成)
|
|
7
|
-
* ============================================================
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
export { COLOR_TYPES, type ColorType } from './generated/constants'
|
|
11
|
-
export * from './runtime/api'
|
|
12
|
-
export { mix, generateElShades } from './runtime/colors'
|
|
1
|
+
/**
|
|
2
|
+
* ============================================================
|
|
3
|
+
* @zl-ui-kit/theme 入口(运行时 API + 常量)
|
|
4
|
+
* 主题 CSS 入口:@zl-ui-kit/theme/light|dark|red-light|red-dark|blue 或 @zl-ui-kit/theme/themes.css
|
|
5
|
+
* SCSS 工具入口:@zl-ui-kit/theme/scss
|
|
6
|
+
* 自定义主题:见 @zl-ui-kit/components 的 README §10 主题定制(L1 变量直覆 / L2 defineTheme / L3 构建时生成)
|
|
7
|
+
* ============================================================
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export { COLOR_TYPES, type ColorType } from './generated/constants'
|
|
11
|
+
export * from './runtime/api'
|
|
12
|
+
export { mix, generateElShades } from './runtime/colors'
|
package/src/runtime/api.ts
CHANGED
|
@@ -1,327 +1,327 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @zl-ui-kit/theme 运行时 API(需求 §7.2.3,D7)
|
|
3
|
-
* L2:defineTheme 运行时定义主题 + setTheme/getTheme/useTheme 切换(可选持久化)
|
|
4
|
-
* 自定义主题与内置主题变量完全同构(--zl-* + --el-*),components 无差别渲染
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import themesDataRaw from '../generated/themes-data.json'
|
|
8
|
-
import { generateElShades, mix } from './colors'
|
|
9
|
-
import type { ComputedRef } from 'vue'
|
|
10
|
-
import { computed, readonly, ref } from 'vue'
|
|
11
|
-
|
|
12
|
-
/** 各主题解析后的 token 值表(构建期由 themegen 生成) */
|
|
13
|
-
export const themesData = themesDataRaw as Record<string, Record<string, string>>
|
|
14
|
-
|
|
15
|
-
/** 内置主题清单(数据源与 generated/constants.ts 一致) */
|
|
16
|
-
export const THEMES = Object.keys(themesData) as ThemeName[]
|
|
17
|
-
|
|
18
|
-
export type ThemeName = string
|
|
19
|
-
|
|
20
|
-
export interface DefineThemeColors {
|
|
21
|
-
/** 主色(EP primary + --zl-primary-6 联动派生) */
|
|
22
|
-
primary?: string
|
|
23
|
-
success?: string
|
|
24
|
-
warning?: string
|
|
25
|
-
danger?: string
|
|
26
|
-
info?: string
|
|
27
|
-
/** 页面底色(覆盖 --zl-bg-1..5 / --el-bg-*) */
|
|
28
|
-
bg?: string
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export interface DefineThemeOptions {
|
|
32
|
-
/** 主题名(切换时使用) */
|
|
33
|
-
name: string
|
|
34
|
-
/** 以哪套内置主题为基线(默认 'light'),未指定的 token 全部继承 */
|
|
35
|
-
base?: string
|
|
36
|
-
/** 少量语义色定制,其余自动派生 */
|
|
37
|
-
colors?: DefineThemeColors
|
|
38
|
-
/** 任意 token 覆盖(键为原始 token 名,如 color-text-1 / color-modo-6) */
|
|
39
|
-
tokens?: Record<string, string>
|
|
40
|
-
/** 是否立即切换到该主题 */
|
|
41
|
-
apply?: boolean
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
const THEME_STORAGE_KEY = 'zl-theme'
|
|
45
|
-
const customThemes = new Map<string, { cssText: string }>()
|
|
46
|
-
const currentTheme = ref<string>(initThemeFromStorage())
|
|
47
|
-
const persistenceEnabled = ref(false)
|
|
48
|
-
const changeListeners = new Set<(theme: string) => void>()
|
|
49
|
-
|
|
50
|
-
function initThemeFromStorage(): string {
|
|
51
|
-
try {
|
|
52
|
-
return localStorage.getItem(THEME_STORAGE_KEY) || 'light'
|
|
53
|
-
} catch {
|
|
54
|
-
return 'light'
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/** 生成某主题完整变量声明文本(--zl-* + --el-* + 布局资产变量) */
|
|
59
|
-
function buildCssVars(resolved: Record<string, string>, themeName: string, baseName = themeName): string {
|
|
60
|
-
const isDark = themeName.includes('dark') || baseName.includes('dark')
|
|
61
|
-
const get = (k: string, fallback = '') => resolved[`color-${k}`] ?? fallback
|
|
62
|
-
const getLevel = (name: string, level: number) => resolved[`color-${name}-${level}`] ?? ''
|
|
63
|
-
|
|
64
|
-
const decls: string[] = []
|
|
65
|
-
|
|
66
|
-
// --zl-* 业务层
|
|
67
|
-
for (const [key, value] of Object.entries(resolved)) {
|
|
68
|
-
decls.push(`--zl-${key.replace(/^color-/, '')}: ${value};`)
|
|
69
|
-
}
|
|
70
|
-
for (let i = 1; i <= 10; i++) {
|
|
71
|
-
decls.push(`--zl-primary-${i}: ${getLevel('modo', i)};`)
|
|
72
|
-
decls.push(`--zl-link-${i}: ${getLevel('link', i)};`)
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
// --el-* EP 层
|
|
76
|
-
const semantic: Array<[string, string]> = [
|
|
77
|
-
['primary', getLevel('modo', 6)],
|
|
78
|
-
['success', getLevel('green', 6)],
|
|
79
|
-
['warning', getLevel('orange', 6)],
|
|
80
|
-
['danger', getLevel('red', 6)],
|
|
81
|
-
// EP 2.14 起 error 是规范名(el-tag--error / el-message--error 走它),
|
|
82
|
-
// 只给 danger 会让这些组件回落到 EP 默认浅色底,深色主题下尤其刺眼
|
|
83
|
-
['error', getLevel('red', 6)],
|
|
84
|
-
['info', getLevel('gray', 6)],
|
|
85
|
-
]
|
|
86
|
-
for (const [name, hex] of semantic) {
|
|
87
|
-
decls.push(`--el-color-${name}: ${hex};`)
|
|
88
|
-
for (const [shade, value] of Object.entries(generateElShades(hex))) {
|
|
89
|
-
decls.push(`--el-color-${name}-${shade}: ${value};`)
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
const textMap: Record<string, string> = {
|
|
94
|
-
primary: 'text-1',
|
|
95
|
-
regular: 'text-2',
|
|
96
|
-
secondary: 'text-3',
|
|
97
|
-
placeholder: 'text-4',
|
|
98
|
-
// 没有 text-5:留空会产出 `--el-text-color-disabled: ;` 这种非法声明,
|
|
99
|
-
// 浏览器整条丢弃,EP 内部又会回落默认值 —— 直接复用 placeholder
|
|
100
|
-
disabled: 'text-4',
|
|
101
|
-
}
|
|
102
|
-
for (const [el, token] of Object.entries(textMap)) decls.push(`--el-text-color-${el}: ${get(token)};`)
|
|
103
|
-
|
|
104
|
-
const borderMap: Array<[string, string]> = [
|
|
105
|
-
['extra-light', 'border-1'],
|
|
106
|
-
['lighter', 'border-2'],
|
|
107
|
-
['light', 'border-3'],
|
|
108
|
-
['', 'border-4'],
|
|
109
|
-
['dark', 'border-4'],
|
|
110
|
-
['darker', 'border-4'],
|
|
111
|
-
]
|
|
112
|
-
for (const [suffix, token] of borderMap)
|
|
113
|
-
decls.push(`--el-border-color${suffix ? `-${suffix}` : ''}: ${get(token)};`)
|
|
114
|
-
|
|
115
|
-
const fillMap: Array<[string, string]> = [
|
|
116
|
-
['extra-light', 'fill-1'],
|
|
117
|
-
['lighter', 'fill-2'],
|
|
118
|
-
['light', 'fill-3'],
|
|
119
|
-
['', 'fill-4'],
|
|
120
|
-
['dark', 'fill-4'],
|
|
121
|
-
]
|
|
122
|
-
for (const [suffix, token] of fillMap)
|
|
123
|
-
decls.push(`--el-fill-color${suffix ? `-${suffix}` : ''}: ${get(token)};`)
|
|
124
|
-
decls.push(`--el-fill-color-blank: ${isDark ? get('bg-2') : '#ffffff'};`)
|
|
125
|
-
|
|
126
|
-
decls.push(`--el-bg-color: ${get('bg-2')};`)
|
|
127
|
-
decls.push(`--el-bg-color-page: ${get('bg-1')};`)
|
|
128
|
-
decls.push(`--el-bg-color-overlay: ${get('bg-3')};`)
|
|
129
|
-
decls.push(`--el-color-white: ${isDark ? '#141414' : '#ffffff'};`)
|
|
130
|
-
decls.push(`--el-color-black: ${isDark ? '#ffffff' : '#000000'};`)
|
|
131
|
-
decls.push(`--el-mask-color: rgb(0, 0, 0, ${isDark ? '0.3' : '0.1'});`)
|
|
132
|
-
decls.push(`color-scheme: ${isDark ? 'dark' : 'light'};`)
|
|
133
|
-
// EP 圆角 ← 形状 token(与内置主题产物同构):`defineTheme({ tokens: { 'radius-base': ... } })`
|
|
134
|
-
// 即可同时驱动 kit 组件与 Element Plus 控件,宿主不必再用 CSS 选择器抢优先级。
|
|
135
|
-
// 基线 radius-base 4px / radius-small 2px 与 EP 默认值相同,内置主题零视觉变化。
|
|
136
|
-
decls.push(`--el-border-radius-base: ${get('radius-base')};`)
|
|
137
|
-
decls.push(`--el-border-radius-small: ${get('radius-small')};`)
|
|
138
|
-
|
|
139
|
-
// 布局 / 菜单资产变量:内置主题由 scripts/themegen.mjs 产出,运行时主题必须同构,
|
|
140
|
-
// 否则 defineTheme 造出的主题会把侧栏与菜单底色整块丢掉(退化成透明)。
|
|
141
|
-
// 浅色/深色基线用弱对比底色;以彩色主题(red-light/blue…)为基线时侧栏是深色渐变,
|
|
142
|
-
// 菜单文字必须转白 —— 所以这里要看基线名,不能只看自定义主题名。
|
|
143
|
-
const isPlainBase = baseName === 'light' || baseName === 'dark'
|
|
144
|
-
const isLightBase = baseName === 'light'
|
|
145
|
-
decls.push(`--zl-menu-hover-bg: ${isPlainBase ? 'var(--zl-fill-2)' : 'rgba(255, 255, 255, 0.2)'};`)
|
|
146
|
-
decls.push(`--zl-menu-text-color: ${isPlainBase ? 'var(--zl-text-1)' : 'var(--zl-white)'};`)
|
|
147
|
-
// 浅色基线:主色选中态 + 浅色侧栏/顶栏(与内置 light 主题同构);
|
|
148
|
-
// 其它基线显式回写中性值,避免继承 body 上的 light 令牌
|
|
149
|
-
if (isLightBase) {
|
|
150
|
-
decls.push('--zl-menu-active-bg: var(--zl-primary-1);')
|
|
151
|
-
decls.push('--zl-menu-active-color: var(--zl-primary-6);')
|
|
152
|
-
decls.push('--zl-menu-active-border-color: var(--zl-primary-6);')
|
|
153
|
-
decls.push('--zl-menu-icon-color: var(--zl-text-3);')
|
|
154
|
-
decls.push('--zl-menu-font-size: 14px;')
|
|
155
|
-
decls.push('--zl-menu-item-height: 40px;')
|
|
156
|
-
} else {
|
|
157
|
-
decls.push('--zl-menu-active-bg: var(--zl-menu-hover-bg);')
|
|
158
|
-
decls.push('--zl-menu-active-color: var(--zl-menu-text-color);')
|
|
159
|
-
decls.push('--zl-menu-active-border-color: transparent;')
|
|
160
|
-
decls.push('--zl-menu-icon-color: currentColor;')
|
|
161
|
-
decls.push('--zl-menu-font-size: 12px;')
|
|
162
|
-
decls.push('--zl-menu-item-height: 36px;')
|
|
163
|
-
}
|
|
164
|
-
decls.push(`--zl-menu-trigger-arrow-color: ${isDark ? '#253044' : '#f6fafc'};`)
|
|
165
|
-
decls.push(`--zl-menu-trigger-bg: linear-gradient(135deg, var(--zl-modo-5), var(--zl-modo-7));`)
|
|
166
|
-
decls.push(
|
|
167
|
-
isLightBase
|
|
168
|
-
? `--zl-layout-vertical-aside-bg: linear-gradient(180deg, var(--zl-primary-1) 0%, var(--zl-bg-1) 100%);`
|
|
169
|
-
: `--zl-layout-vertical-aside-bg: linear-gradient(180deg, var(--zl-modo-7) 0%, var(--zl-modo-9) 100%);`,
|
|
170
|
-
)
|
|
171
|
-
decls.push(
|
|
172
|
-
isLightBase
|
|
173
|
-
? `--zl-layout-horizontal-header-bg: var(--zl-bg-1);`
|
|
174
|
-
: `--zl-layout-horizontal-header-bg: linear-gradient(90deg, var(--zl-modo-7) 0%, var(--zl-modo-6) 60%, var(--zl-modo-5) 100%);`,
|
|
175
|
-
)
|
|
176
|
-
|
|
177
|
-
return decls.join('\n ')
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
function injectStyle(name: string, cssText: string) {
|
|
181
|
-
const styleId = `zl-theme-style-${name}`
|
|
182
|
-
let style = document.getElementById(styleId) as HTMLStyleElement | null
|
|
183
|
-
if (!style) {
|
|
184
|
-
style = document.createElement('style')
|
|
185
|
-
style.id = styleId
|
|
186
|
-
document.head.appendChild(style)
|
|
187
|
-
}
|
|
188
|
-
style.textContent = cssText
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
/**
|
|
192
|
-
* 运行时定义主题(L2)
|
|
193
|
-
* 以某内置主题为基线,覆盖语义色后自动派生 EP 色阶与业务变量并注入样式
|
|
194
|
-
*/
|
|
195
|
-
export function defineTheme(options: DefineThemeOptions): void {
|
|
196
|
-
const { name, base = 'light', colors = {}, tokens = {}, apply = false } = options
|
|
197
|
-
if (THEMES.includes(name) && !customThemes.has(name)) {
|
|
198
|
-
throw new Error(`主题 ${name} 为内置主题,不可覆盖`)
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
// 基线数据克隆
|
|
202
|
-
const resolved: Record<string, string> = { ...(themesData[base] ?? themesData.light) }
|
|
203
|
-
for (const [key, value] of Object.entries(tokens)) {
|
|
204
|
-
resolved[`color-${key.replace(/^color-/, '')}`] = value
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
// 形状 / 字体 token:与内置主题同构产出(值表与 scripts/themegen.mjs 的 SHAPE_TOKENS 一致,
|
|
208
|
-
// 两边漂移会让 `defineTheme` 造出的主题与内置主题观感不同)。
|
|
209
|
-
// 组件里写的是 `var(--zl-radius-large, 6px)` 这类带兜底的形式,此前这些 token 从未产出,
|
|
210
|
-
// 等于「值被兜底写死、宿主改不了」。用 `??=` 播种:`tokens: { 'radius-large': '10px' }` 这类覆盖仍然生效。
|
|
211
|
-
const shapeTokens: Record<string, string> = {
|
|
212
|
-
'radius-small': '2px',
|
|
213
|
-
'radius-base': '4px',
|
|
214
|
-
'radius-medium': '4px',
|
|
215
|
-
'radius-large': '6px',
|
|
216
|
-
'radius-xl': '12px',
|
|
217
|
-
'radius-round': '99px',
|
|
218
|
-
'font-family-code': "Consolas, 'SFMono-Regular', Menlo, monospace",
|
|
219
|
-
'slogan-font-family': "'PingFang SC', 'Microsoft YaHei', sans-serif",
|
|
220
|
-
}
|
|
221
|
-
for (const [key, value] of Object.entries(shapeTokens)) {
|
|
222
|
-
resolved[`color-${key}`] ??= value
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
const colorEntries: Array<[keyof DefineThemeColors, string, string]> = [
|
|
226
|
-
['primary', 'modo', '6'],
|
|
227
|
-
['success', 'green', '6'],
|
|
228
|
-
['warning', 'orange', '6'],
|
|
229
|
-
['danger', 'red', '6'],
|
|
230
|
-
['info', 'gray', '6'],
|
|
231
|
-
]
|
|
232
|
-
for (const [semantic, palette, level] of colorEntries) {
|
|
233
|
-
const hex = colors[semantic]
|
|
234
|
-
if (hex) {
|
|
235
|
-
resolved[`color-${palette}-${level}`] = hex
|
|
236
|
-
// 主色同时派生整组色板(浅→白、深→黑,各 10 级),保证 --zl-{palette}-N 可用
|
|
237
|
-
const seedLevel = Number(level)
|
|
238
|
-
for (let i = 1; i <= 10; i++) {
|
|
239
|
-
if (i === seedLevel) continue
|
|
240
|
-
const weight = (Math.abs(i - seedLevel) / 5) * 50
|
|
241
|
-
resolved[`color-${palette}-${i}`] =
|
|
242
|
-
i < seedLevel ? mix('#ffffff', hex, 100 - weight) : mix('#000000', hex, 100 - weight)
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
if (colors.bg) {
|
|
248
|
-
resolved['color-bg-1'] = colors.bg
|
|
249
|
-
resolved['color-bg-2'] = colors.bg
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
const cssText = `body[data-theme='${name}'] {\n ${buildCssVars(resolved, name, base)}\n}`
|
|
253
|
-
injectStyle(name, cssText)
|
|
254
|
-
customThemes.set(name, { cssText })
|
|
255
|
-
if (!THEMES.includes(name)) {
|
|
256
|
-
THEMES.push(name)
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
if (apply) {
|
|
260
|
-
setTheme(name)
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
export interface SetThemeOptions {
|
|
265
|
-
/** 本次切换是否持久化(需已启用持久化或显式指定) */
|
|
266
|
-
persist?: boolean
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
/** 切换主题(内置/自定义均可;light 作用于 body 本身) */
|
|
270
|
-
export function setTheme(name: string, options: SetThemeOptions = {}): void {
|
|
271
|
-
currentTheme.value = name
|
|
272
|
-
if (name === 'light') {
|
|
273
|
-
document.body.removeAttribute('data-theme')
|
|
274
|
-
} else {
|
|
275
|
-
document.body.setAttribute('data-theme', name)
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
if (persistenceEnabled.value || options.persist) {
|
|
279
|
-
try {
|
|
280
|
-
localStorage.setItem(THEME_STORAGE_KEY, name)
|
|
281
|
-
} catch {
|
|
282
|
-
// storage 不可用时忽略
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
changeListeners.forEach((fn) => fn(name))
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
/** 当前主题名 */
|
|
290
|
-
export function getTheme(): string {
|
|
291
|
-
return currentTheme.value
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
/** 全部可用主题(内置 + 运行时自定义) */
|
|
295
|
-
export function getThemes(): string[] {
|
|
296
|
-
return [...THEMES]
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
/** 开启主题持久化(同一浏览器记住所选主题) */
|
|
300
|
-
export function enableThemePersistence() {
|
|
301
|
-
persistenceEnabled.value = true
|
|
302
|
-
setTheme(currentTheme.value, { persist: true })
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
/** 主题变化监听 */
|
|
306
|
-
export function onThemeChange(callback: (theme: string) => void): () => void {
|
|
307
|
-
changeListeners.add(callback)
|
|
308
|
-
return () => changeListeners.delete(callback)
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
export interface UseThemeReturn {
|
|
312
|
-
/** 当前主题(只读 ref) */
|
|
313
|
-
theme: ComputedRef<string>
|
|
314
|
-
/** 可用主题列表(只读) */
|
|
315
|
-
themes: ComputedRef<string[]>
|
|
316
|
-
/** 切换主题 */
|
|
317
|
-
setTheme: (name: string, options?: SetThemeOptions) => void
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
/** 主题 composable(配合 defineTheme 实现运行时换肤) */
|
|
321
|
-
export function useTheme(): UseThemeReturn {
|
|
322
|
-
return {
|
|
323
|
-
theme: readonly(currentTheme) as unknown as ComputedRef<string>,
|
|
324
|
-
themes: computed(() => getThemes()),
|
|
325
|
-
setTheme,
|
|
326
|
-
}
|
|
327
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* @zl-ui-kit/theme 运行时 API(需求 §7.2.3,D7)
|
|
3
|
+
* L2:defineTheme 运行时定义主题 + setTheme/getTheme/useTheme 切换(可选持久化)
|
|
4
|
+
* 自定义主题与内置主题变量完全同构(--zl-* + --el-*),components 无差别渲染
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import themesDataRaw from '../generated/themes-data.json'
|
|
8
|
+
import { generateElShades, mix } from './colors'
|
|
9
|
+
import type { ComputedRef } from 'vue'
|
|
10
|
+
import { computed, readonly, ref } from 'vue'
|
|
11
|
+
|
|
12
|
+
/** 各主题解析后的 token 值表(构建期由 themegen 生成) */
|
|
13
|
+
export const themesData = themesDataRaw as Record<string, Record<string, string>>
|
|
14
|
+
|
|
15
|
+
/** 内置主题清单(数据源与 generated/constants.ts 一致) */
|
|
16
|
+
export const THEMES = Object.keys(themesData) as ThemeName[]
|
|
17
|
+
|
|
18
|
+
export type ThemeName = string
|
|
19
|
+
|
|
20
|
+
export interface DefineThemeColors {
|
|
21
|
+
/** 主色(EP primary + --zl-primary-6 联动派生) */
|
|
22
|
+
primary?: string
|
|
23
|
+
success?: string
|
|
24
|
+
warning?: string
|
|
25
|
+
danger?: string
|
|
26
|
+
info?: string
|
|
27
|
+
/** 页面底色(覆盖 --zl-bg-1..5 / --el-bg-*) */
|
|
28
|
+
bg?: string
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface DefineThemeOptions {
|
|
32
|
+
/** 主题名(切换时使用) */
|
|
33
|
+
name: string
|
|
34
|
+
/** 以哪套内置主题为基线(默认 'light'),未指定的 token 全部继承 */
|
|
35
|
+
base?: string
|
|
36
|
+
/** 少量语义色定制,其余自动派生 */
|
|
37
|
+
colors?: DefineThemeColors
|
|
38
|
+
/** 任意 token 覆盖(键为原始 token 名,如 color-text-1 / color-modo-6) */
|
|
39
|
+
tokens?: Record<string, string>
|
|
40
|
+
/** 是否立即切换到该主题 */
|
|
41
|
+
apply?: boolean
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const THEME_STORAGE_KEY = 'zl-theme'
|
|
45
|
+
const customThemes = new Map<string, { cssText: string }>()
|
|
46
|
+
const currentTheme = ref<string>(initThemeFromStorage())
|
|
47
|
+
const persistenceEnabled = ref(false)
|
|
48
|
+
const changeListeners = new Set<(theme: string) => void>()
|
|
49
|
+
|
|
50
|
+
function initThemeFromStorage(): string {
|
|
51
|
+
try {
|
|
52
|
+
return localStorage.getItem(THEME_STORAGE_KEY) || 'light'
|
|
53
|
+
} catch {
|
|
54
|
+
return 'light'
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** 生成某主题完整变量声明文本(--zl-* + --el-* + 布局资产变量) */
|
|
59
|
+
function buildCssVars(resolved: Record<string, string>, themeName: string, baseName = themeName): string {
|
|
60
|
+
const isDark = themeName.includes('dark') || baseName.includes('dark')
|
|
61
|
+
const get = (k: string, fallback = '') => resolved[`color-${k}`] ?? fallback
|
|
62
|
+
const getLevel = (name: string, level: number) => resolved[`color-${name}-${level}`] ?? ''
|
|
63
|
+
|
|
64
|
+
const decls: string[] = []
|
|
65
|
+
|
|
66
|
+
// --zl-* 业务层
|
|
67
|
+
for (const [key, value] of Object.entries(resolved)) {
|
|
68
|
+
decls.push(`--zl-${key.replace(/^color-/, '')}: ${value};`)
|
|
69
|
+
}
|
|
70
|
+
for (let i = 1; i <= 10; i++) {
|
|
71
|
+
decls.push(`--zl-primary-${i}: ${getLevel('modo', i)};`)
|
|
72
|
+
decls.push(`--zl-link-${i}: ${getLevel('link', i)};`)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// --el-* EP 层
|
|
76
|
+
const semantic: Array<[string, string]> = [
|
|
77
|
+
['primary', getLevel('modo', 6)],
|
|
78
|
+
['success', getLevel('green', 6)],
|
|
79
|
+
['warning', getLevel('orange', 6)],
|
|
80
|
+
['danger', getLevel('red', 6)],
|
|
81
|
+
// EP 2.14 起 error 是规范名(el-tag--error / el-message--error 走它),
|
|
82
|
+
// 只给 danger 会让这些组件回落到 EP 默认浅色底,深色主题下尤其刺眼
|
|
83
|
+
['error', getLevel('red', 6)],
|
|
84
|
+
['info', getLevel('gray', 6)],
|
|
85
|
+
]
|
|
86
|
+
for (const [name, hex] of semantic) {
|
|
87
|
+
decls.push(`--el-color-${name}: ${hex};`)
|
|
88
|
+
for (const [shade, value] of Object.entries(generateElShades(hex))) {
|
|
89
|
+
decls.push(`--el-color-${name}-${shade}: ${value};`)
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const textMap: Record<string, string> = {
|
|
94
|
+
primary: 'text-1',
|
|
95
|
+
regular: 'text-2',
|
|
96
|
+
secondary: 'text-3',
|
|
97
|
+
placeholder: 'text-4',
|
|
98
|
+
// 没有 text-5:留空会产出 `--el-text-color-disabled: ;` 这种非法声明,
|
|
99
|
+
// 浏览器整条丢弃,EP 内部又会回落默认值 —— 直接复用 placeholder
|
|
100
|
+
disabled: 'text-4',
|
|
101
|
+
}
|
|
102
|
+
for (const [el, token] of Object.entries(textMap)) decls.push(`--el-text-color-${el}: ${get(token)};`)
|
|
103
|
+
|
|
104
|
+
const borderMap: Array<[string, string]> = [
|
|
105
|
+
['extra-light', 'border-1'],
|
|
106
|
+
['lighter', 'border-2'],
|
|
107
|
+
['light', 'border-3'],
|
|
108
|
+
['', 'border-4'],
|
|
109
|
+
['dark', 'border-4'],
|
|
110
|
+
['darker', 'border-4'],
|
|
111
|
+
]
|
|
112
|
+
for (const [suffix, token] of borderMap)
|
|
113
|
+
decls.push(`--el-border-color${suffix ? `-${suffix}` : ''}: ${get(token)};`)
|
|
114
|
+
|
|
115
|
+
const fillMap: Array<[string, string]> = [
|
|
116
|
+
['extra-light', 'fill-1'],
|
|
117
|
+
['lighter', 'fill-2'],
|
|
118
|
+
['light', 'fill-3'],
|
|
119
|
+
['', 'fill-4'],
|
|
120
|
+
['dark', 'fill-4'],
|
|
121
|
+
]
|
|
122
|
+
for (const [suffix, token] of fillMap)
|
|
123
|
+
decls.push(`--el-fill-color${suffix ? `-${suffix}` : ''}: ${get(token)};`)
|
|
124
|
+
decls.push(`--el-fill-color-blank: ${isDark ? get('bg-2') : '#ffffff'};`)
|
|
125
|
+
|
|
126
|
+
decls.push(`--el-bg-color: ${get('bg-2')};`)
|
|
127
|
+
decls.push(`--el-bg-color-page: ${get('bg-1')};`)
|
|
128
|
+
decls.push(`--el-bg-color-overlay: ${get('bg-3')};`)
|
|
129
|
+
decls.push(`--el-color-white: ${isDark ? '#141414' : '#ffffff'};`)
|
|
130
|
+
decls.push(`--el-color-black: ${isDark ? '#ffffff' : '#000000'};`)
|
|
131
|
+
decls.push(`--el-mask-color: rgb(0, 0, 0, ${isDark ? '0.3' : '0.1'});`)
|
|
132
|
+
decls.push(`color-scheme: ${isDark ? 'dark' : 'light'};`)
|
|
133
|
+
// EP 圆角 ← 形状 token(与内置主题产物同构):`defineTheme({ tokens: { 'radius-base': ... } })`
|
|
134
|
+
// 即可同时驱动 kit 组件与 Element Plus 控件,宿主不必再用 CSS 选择器抢优先级。
|
|
135
|
+
// 基线 radius-base 4px / radius-small 2px 与 EP 默认值相同,内置主题零视觉变化。
|
|
136
|
+
decls.push(`--el-border-radius-base: ${get('radius-base')};`)
|
|
137
|
+
decls.push(`--el-border-radius-small: ${get('radius-small')};`)
|
|
138
|
+
|
|
139
|
+
// 布局 / 菜单资产变量:内置主题由 scripts/themegen.mjs 产出,运行时主题必须同构,
|
|
140
|
+
// 否则 defineTheme 造出的主题会把侧栏与菜单底色整块丢掉(退化成透明)。
|
|
141
|
+
// 浅色/深色基线用弱对比底色;以彩色主题(red-light/blue…)为基线时侧栏是深色渐变,
|
|
142
|
+
// 菜单文字必须转白 —— 所以这里要看基线名,不能只看自定义主题名。
|
|
143
|
+
const isPlainBase = baseName === 'light' || baseName === 'dark'
|
|
144
|
+
const isLightBase = baseName === 'light'
|
|
145
|
+
decls.push(`--zl-menu-hover-bg: ${isPlainBase ? 'var(--zl-fill-2)' : 'rgba(255, 255, 255, 0.2)'};`)
|
|
146
|
+
decls.push(`--zl-menu-text-color: ${isPlainBase ? 'var(--zl-text-1)' : 'var(--zl-white)'};`)
|
|
147
|
+
// 浅色基线:主色选中态 + 浅色侧栏/顶栏(与内置 light 主题同构);
|
|
148
|
+
// 其它基线显式回写中性值,避免继承 body 上的 light 令牌
|
|
149
|
+
if (isLightBase) {
|
|
150
|
+
decls.push('--zl-menu-active-bg: var(--zl-primary-1);')
|
|
151
|
+
decls.push('--zl-menu-active-color: var(--zl-primary-6);')
|
|
152
|
+
decls.push('--zl-menu-active-border-color: var(--zl-primary-6);')
|
|
153
|
+
decls.push('--zl-menu-icon-color: var(--zl-text-3);')
|
|
154
|
+
decls.push('--zl-menu-font-size: 14px;')
|
|
155
|
+
decls.push('--zl-menu-item-height: 40px;')
|
|
156
|
+
} else {
|
|
157
|
+
decls.push('--zl-menu-active-bg: var(--zl-menu-hover-bg);')
|
|
158
|
+
decls.push('--zl-menu-active-color: var(--zl-menu-text-color);')
|
|
159
|
+
decls.push('--zl-menu-active-border-color: transparent;')
|
|
160
|
+
decls.push('--zl-menu-icon-color: currentColor;')
|
|
161
|
+
decls.push('--zl-menu-font-size: 12px;')
|
|
162
|
+
decls.push('--zl-menu-item-height: 36px;')
|
|
163
|
+
}
|
|
164
|
+
decls.push(`--zl-menu-trigger-arrow-color: ${isDark ? '#253044' : '#f6fafc'};`)
|
|
165
|
+
decls.push(`--zl-menu-trigger-bg: linear-gradient(135deg, var(--zl-modo-5), var(--zl-modo-7));`)
|
|
166
|
+
decls.push(
|
|
167
|
+
isLightBase
|
|
168
|
+
? `--zl-layout-vertical-aside-bg: linear-gradient(180deg, var(--zl-primary-1) 0%, var(--zl-bg-1) 100%);`
|
|
169
|
+
: `--zl-layout-vertical-aside-bg: linear-gradient(180deg, var(--zl-modo-7) 0%, var(--zl-modo-9) 100%);`,
|
|
170
|
+
)
|
|
171
|
+
decls.push(
|
|
172
|
+
isLightBase
|
|
173
|
+
? `--zl-layout-horizontal-header-bg: var(--zl-bg-1);`
|
|
174
|
+
: `--zl-layout-horizontal-header-bg: linear-gradient(90deg, var(--zl-modo-7) 0%, var(--zl-modo-6) 60%, var(--zl-modo-5) 100%);`,
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
return decls.join('\n ')
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function injectStyle(name: string, cssText: string) {
|
|
181
|
+
const styleId = `zl-theme-style-${name}`
|
|
182
|
+
let style = document.getElementById(styleId) as HTMLStyleElement | null
|
|
183
|
+
if (!style) {
|
|
184
|
+
style = document.createElement('style')
|
|
185
|
+
style.id = styleId
|
|
186
|
+
document.head.appendChild(style)
|
|
187
|
+
}
|
|
188
|
+
style.textContent = cssText
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* 运行时定义主题(L2)
|
|
193
|
+
* 以某内置主题为基线,覆盖语义色后自动派生 EP 色阶与业务变量并注入样式
|
|
194
|
+
*/
|
|
195
|
+
export function defineTheme(options: DefineThemeOptions): void {
|
|
196
|
+
const { name, base = 'light', colors = {}, tokens = {}, apply = false } = options
|
|
197
|
+
if (THEMES.includes(name) && !customThemes.has(name)) {
|
|
198
|
+
throw new Error(`主题 ${name} 为内置主题,不可覆盖`)
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// 基线数据克隆
|
|
202
|
+
const resolved: Record<string, string> = { ...(themesData[base] ?? themesData.light) }
|
|
203
|
+
for (const [key, value] of Object.entries(tokens)) {
|
|
204
|
+
resolved[`color-${key.replace(/^color-/, '')}`] = value
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// 形状 / 字体 token:与内置主题同构产出(值表与 scripts/themegen.mjs 的 SHAPE_TOKENS 一致,
|
|
208
|
+
// 两边漂移会让 `defineTheme` 造出的主题与内置主题观感不同)。
|
|
209
|
+
// 组件里写的是 `var(--zl-radius-large, 6px)` 这类带兜底的形式,此前这些 token 从未产出,
|
|
210
|
+
// 等于「值被兜底写死、宿主改不了」。用 `??=` 播种:`tokens: { 'radius-large': '10px' }` 这类覆盖仍然生效。
|
|
211
|
+
const shapeTokens: Record<string, string> = {
|
|
212
|
+
'radius-small': '2px',
|
|
213
|
+
'radius-base': '4px',
|
|
214
|
+
'radius-medium': '4px',
|
|
215
|
+
'radius-large': '6px',
|
|
216
|
+
'radius-xl': '12px',
|
|
217
|
+
'radius-round': '99px',
|
|
218
|
+
'font-family-code': "Consolas, 'SFMono-Regular', Menlo, monospace",
|
|
219
|
+
'slogan-font-family': "'PingFang SC', 'Microsoft YaHei', sans-serif",
|
|
220
|
+
}
|
|
221
|
+
for (const [key, value] of Object.entries(shapeTokens)) {
|
|
222
|
+
resolved[`color-${key}`] ??= value
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const colorEntries: Array<[keyof DefineThemeColors, string, string]> = [
|
|
226
|
+
['primary', 'modo', '6'],
|
|
227
|
+
['success', 'green', '6'],
|
|
228
|
+
['warning', 'orange', '6'],
|
|
229
|
+
['danger', 'red', '6'],
|
|
230
|
+
['info', 'gray', '6'],
|
|
231
|
+
]
|
|
232
|
+
for (const [semantic, palette, level] of colorEntries) {
|
|
233
|
+
const hex = colors[semantic]
|
|
234
|
+
if (hex) {
|
|
235
|
+
resolved[`color-${palette}-${level}`] = hex
|
|
236
|
+
// 主色同时派生整组色板(浅→白、深→黑,各 10 级),保证 --zl-{palette}-N 可用
|
|
237
|
+
const seedLevel = Number(level)
|
|
238
|
+
for (let i = 1; i <= 10; i++) {
|
|
239
|
+
if (i === seedLevel) continue
|
|
240
|
+
const weight = (Math.abs(i - seedLevel) / 5) * 50
|
|
241
|
+
resolved[`color-${palette}-${i}`] =
|
|
242
|
+
i < seedLevel ? mix('#ffffff', hex, 100 - weight) : mix('#000000', hex, 100 - weight)
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (colors.bg) {
|
|
248
|
+
resolved['color-bg-1'] = colors.bg
|
|
249
|
+
resolved['color-bg-2'] = colors.bg
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const cssText = `body[data-theme='${name}'] {\n ${buildCssVars(resolved, name, base)}\n}`
|
|
253
|
+
injectStyle(name, cssText)
|
|
254
|
+
customThemes.set(name, { cssText })
|
|
255
|
+
if (!THEMES.includes(name)) {
|
|
256
|
+
THEMES.push(name)
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (apply) {
|
|
260
|
+
setTheme(name)
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
export interface SetThemeOptions {
|
|
265
|
+
/** 本次切换是否持久化(需已启用持久化或显式指定) */
|
|
266
|
+
persist?: boolean
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/** 切换主题(内置/自定义均可;light 作用于 body 本身) */
|
|
270
|
+
export function setTheme(name: string, options: SetThemeOptions = {}): void {
|
|
271
|
+
currentTheme.value = name
|
|
272
|
+
if (name === 'light') {
|
|
273
|
+
document.body.removeAttribute('data-theme')
|
|
274
|
+
} else {
|
|
275
|
+
document.body.setAttribute('data-theme', name)
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
if (persistenceEnabled.value || options.persist) {
|
|
279
|
+
try {
|
|
280
|
+
localStorage.setItem(THEME_STORAGE_KEY, name)
|
|
281
|
+
} catch {
|
|
282
|
+
// storage 不可用时忽略
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
changeListeners.forEach((fn) => fn(name))
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/** 当前主题名 */
|
|
290
|
+
export function getTheme(): string {
|
|
291
|
+
return currentTheme.value
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/** 全部可用主题(内置 + 运行时自定义) */
|
|
295
|
+
export function getThemes(): string[] {
|
|
296
|
+
return [...THEMES]
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/** 开启主题持久化(同一浏览器记住所选主题) */
|
|
300
|
+
export function enableThemePersistence() {
|
|
301
|
+
persistenceEnabled.value = true
|
|
302
|
+
setTheme(currentTheme.value, { persist: true })
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/** 主题变化监听 */
|
|
306
|
+
export function onThemeChange(callback: (theme: string) => void): () => void {
|
|
307
|
+
changeListeners.add(callback)
|
|
308
|
+
return () => changeListeners.delete(callback)
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
export interface UseThemeReturn {
|
|
312
|
+
/** 当前主题(只读 ref) */
|
|
313
|
+
theme: ComputedRef<string>
|
|
314
|
+
/** 可用主题列表(只读) */
|
|
315
|
+
themes: ComputedRef<string[]>
|
|
316
|
+
/** 切换主题 */
|
|
317
|
+
setTheme: (name: string, options?: SetThemeOptions) => void
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/** 主题 composable(配合 defineTheme 实现运行时换肤) */
|
|
321
|
+
export function useTheme(): UseThemeReturn {
|
|
322
|
+
return {
|
|
323
|
+
theme: readonly(currentTheme) as unknown as ComputedRef<string>,
|
|
324
|
+
themes: computed(() => getThemes()),
|
|
325
|
+
setTheme,
|
|
326
|
+
}
|
|
327
|
+
}
|