@veltra/styles 1.0.0
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/dist/_functions.scss +49 -0
- package/dist/_mixins.scss +177 -0
- package/dist/_vars.scss +44 -0
- package/dist/anime/fade.css +9 -0
- package/dist/anime/fade.scss +8 -0
- package/dist/anime/slide.css +18 -0
- package/dist/anime/slide.scss +28 -0
- package/dist/anime/spring.css +30 -0
- package/dist/anime/spring.scss +42 -0
- package/dist/anime/zoom-in.css +57 -0
- package/dist/anime/zoom-in.scss +73 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +5 -0
- package/dist/load-theme.d.ts +18 -0
- package/dist/load-theme.js +38 -0
- package/dist/load-theme.js.map +1 -0
- package/dist/normalize.css +76 -0
- package/dist/normalize.scss +63 -0
- package/dist/theme/dark.d.ts +7 -0
- package/dist/theme/dark.js +47 -0
- package/dist/theme/dark.js.map +1 -0
- package/dist/theme/helper.d.ts +22 -0
- package/dist/theme/helper.js +57 -0
- package/dist/theme/helper.js.map +1 -0
- package/dist/theme/index.d.ts +7 -0
- package/dist/theme/index.js +6 -0
- package/dist/theme/light.d.ts +7 -0
- package/dist/theme/light.js +89 -0
- package/dist/theme/light.js.map +1 -0
- package/dist/theme/type.d.ts +91 -0
- package/dist/theme/ui-theme.d.ts +35 -0
- package/dist/theme/ui-theme.js +179 -0
- package/dist/theme/ui-theme.js.map +1 -0
- package/package.json +82 -0
- package/src/_functions.scss +49 -0
- package/src/_mixins.scss +177 -0
- package/src/_vars.scss +44 -0
- package/src/anime/fade.scss +8 -0
- package/src/anime/slide.scss +28 -0
- package/src/anime/spring.scss +42 -0
- package/src/anime/zoom-in.scss +73 -0
- package/src/env.d.ts +1 -0
- package/src/index.ts +5 -0
- package/src/load-theme.ts +46 -0
- package/src/normalize.scss +63 -0
- package/src/theme/__test__/ui-theme.test.ts +27 -0
- package/src/theme/dark.ts +38 -0
- package/src/theme/helper.ts +58 -0
- package/src/theme/index.ts +6 -0
- package/src/theme/light.ts +60 -0
- package/src/theme/type.ts +156 -0
- package/src/theme/ui-theme.ts +265 -0
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { RGBColor } from './type'
|
|
2
|
+
|
|
3
|
+
/** 实现十六进制颜色转RGB颜色,包括透明度 */
|
|
4
|
+
export function HEXToRGB(color: string): RGBColor {
|
|
5
|
+
// 移除可能存在的 '#' 前缀
|
|
6
|
+
let hex = color.replace(/^#/, '')
|
|
7
|
+
let [r, g, b] = [0, 0, 0]
|
|
8
|
+
|
|
9
|
+
if (hex.length === 3) {
|
|
10
|
+
const [r10, g10, b10] = [hex[0]!, hex[1]!, hex[2]!]
|
|
11
|
+
hex = `${r10}${r10}${g10}${g10}${b10}${b10}`
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
r = parseInt(hex.slice(0, 2), 16)
|
|
15
|
+
g = parseInt(hex.slice(2, 4), 16)
|
|
16
|
+
b = parseInt(hex.slice(4, 6), 16)
|
|
17
|
+
|
|
18
|
+
return [r, g, b]
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* 混合两个颜色,并返回混合结果的十六进制表示。
|
|
23
|
+
* @param color1 - 第一个颜色,格式为`#RRGGBB`。
|
|
24
|
+
* @param color2 - 第二个颜色,格式为`#RRGGBB`。
|
|
25
|
+
* @param ratio - 颜色混合的比例,取值范围为0到1。
|
|
26
|
+
* @returns 混合结果的十六进制表示。
|
|
27
|
+
*/
|
|
28
|
+
export function mixColor(color1: `#${string}`, color2: `#${string}`, ratio: number): string {
|
|
29
|
+
if (ratio > 1) throw new Error('ratio的值在0-1之间')
|
|
30
|
+
const color1RGB = HEXToRGB(color1)
|
|
31
|
+
const color2RGB = HEXToRGB(color2)
|
|
32
|
+
|
|
33
|
+
const color1Ratio = 1 - ratio
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
'#' +
|
|
37
|
+
color1RGB
|
|
38
|
+
.map((n, i) => {
|
|
39
|
+
const hex = Math.floor(color1Ratio * n + color2RGB[i]! * ratio).toString(16)
|
|
40
|
+
return hex.length === 1 ? '0' + hex : hex
|
|
41
|
+
})
|
|
42
|
+
.join('')
|
|
43
|
+
)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function defineBySize(
|
|
47
|
+
variable: Record<'small' | 'default' | 'large', number>
|
|
48
|
+
): Record<'small' | 'default' | 'large', number> {
|
|
49
|
+
return variable
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* 引用全局主题 CSS 变量(`--u-*` 命名空间)
|
|
54
|
+
* @param prop - 与 Theme 结构对应的连字符路径,如 `text-color-title`、`bg-color-hover`
|
|
55
|
+
*/
|
|
56
|
+
export function cssVar(prop: string): string {
|
|
57
|
+
return `var(--u-${prop})`
|
|
58
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { UITheme } from './ui-theme'
|
|
2
|
+
export { lightTheme } from './light'
|
|
3
|
+
export { darkTheme } from './dark'
|
|
4
|
+
export type * from './type'
|
|
5
|
+
export { cssVar, defineBySize, HEXToRGB, mixColor } from './helper'
|
|
6
|
+
export { currentTheme, loadTheme, setTheme } from '../load-theme'
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { defineBySize } from './helper'
|
|
2
|
+
import { UITheme } from './ui-theme'
|
|
3
|
+
|
|
4
|
+
export const lightTheme: UITheme = new UITheme(
|
|
5
|
+
{
|
|
6
|
+
color: {
|
|
7
|
+
primary: '#1E88E5',
|
|
8
|
+
success: '#2ba471',
|
|
9
|
+
warning: '#e37318',
|
|
10
|
+
danger: '#d54941',
|
|
11
|
+
info: '#009688',
|
|
12
|
+
disabled: '#f5f7fa',
|
|
13
|
+
default: '#f1f5f9'
|
|
14
|
+
},
|
|
15
|
+
|
|
16
|
+
bg: {
|
|
17
|
+
color: {
|
|
18
|
+
bottom: '#f5f5f5',
|
|
19
|
+
middle: '#fafafa',
|
|
20
|
+
top: '#ffffff',
|
|
21
|
+
hover: '#f5f7fa',
|
|
22
|
+
black: '#000000'
|
|
23
|
+
},
|
|
24
|
+
|
|
25
|
+
filter: { blur: 'blur(16px)', saturate: 'saturate(180%)' }
|
|
26
|
+
},
|
|
27
|
+
|
|
28
|
+
border: { color: '#dcdfe6', width: 1, style: 'solid' },
|
|
29
|
+
|
|
30
|
+
'text-color': {
|
|
31
|
+
title: '#303133',
|
|
32
|
+
main: '#606266',
|
|
33
|
+
placeholder: '#a8abb2',
|
|
34
|
+
second: '#979797',
|
|
35
|
+
assist: '#c0c4cc',
|
|
36
|
+
disabled: '#a8abb2',
|
|
37
|
+
white: '#fff'
|
|
38
|
+
},
|
|
39
|
+
|
|
40
|
+
radius: defineBySize({ small: 4, default: 6, large: 8 }),
|
|
41
|
+
|
|
42
|
+
'form-component-height': defineBySize({ small: 24, default: 32, large: 40 }),
|
|
43
|
+
|
|
44
|
+
'font-family':
|
|
45
|
+
'system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif',
|
|
46
|
+
|
|
47
|
+
'font-size-title': defineBySize({ small: 16, default: 16, large: 18 }),
|
|
48
|
+
|
|
49
|
+
'font-size-main': defineBySize({ small: 12, default: 14, large: 16 }),
|
|
50
|
+
|
|
51
|
+
'font-size-assist': defineBySize({ small: 12, default: 12, large: 14 }),
|
|
52
|
+
|
|
53
|
+
shadow: { color: '#0000001a', x: 0, y: 0, blur: 4, spread: 1 },
|
|
54
|
+
|
|
55
|
+
gap: defineBySize({ small: 6, default: 8, large: 12 }),
|
|
56
|
+
|
|
57
|
+
breakpoint: { xs: 600, sm: 960, md: 1280, lg: 1920 }
|
|
58
|
+
},
|
|
59
|
+
{ reactive: false }
|
|
60
|
+
)
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RGB颜色[红,绿,蓝]
|
|
3
|
+
*/
|
|
4
|
+
export type RGBColor = [number, number, number]
|
|
5
|
+
|
|
6
|
+
/** 仅含全局 token;组件级 token 由各组件 SCSS 以 `--u-{component}-*` 声明 */
|
|
7
|
+
export type Theme = {
|
|
8
|
+
/** 主题色 */
|
|
9
|
+
color: {
|
|
10
|
+
/** 主要颜色 */
|
|
11
|
+
primary: string
|
|
12
|
+
/** 成功颜色 */
|
|
13
|
+
success: string
|
|
14
|
+
/** 警告颜色 */
|
|
15
|
+
warning: string
|
|
16
|
+
/** 危险颜色 */
|
|
17
|
+
danger: string
|
|
18
|
+
/** 信息颜色 */
|
|
19
|
+
info: string
|
|
20
|
+
/** 禁用颜色 */
|
|
21
|
+
disabled: string
|
|
22
|
+
/** 默认颜色 */
|
|
23
|
+
default: string
|
|
24
|
+
}
|
|
25
|
+
/** 背景 */
|
|
26
|
+
bg: {
|
|
27
|
+
/** 背景色 */
|
|
28
|
+
color: {
|
|
29
|
+
/** 底部背景色 */
|
|
30
|
+
bottom: string
|
|
31
|
+
/** 中部背景色 */
|
|
32
|
+
middle: string
|
|
33
|
+
/** 顶部背景色 */
|
|
34
|
+
top: string
|
|
35
|
+
/** 悬停背景色 */
|
|
36
|
+
hover: string
|
|
37
|
+
/** 黑色背景 */
|
|
38
|
+
black: string
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
filter: {
|
|
42
|
+
/** 背景模糊 */
|
|
43
|
+
blur: string
|
|
44
|
+
/** 背景饱和度 */
|
|
45
|
+
saturate: string
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
border: {
|
|
50
|
+
/** 边框颜色 */
|
|
51
|
+
color: string
|
|
52
|
+
/** 边框宽度 */
|
|
53
|
+
width: number
|
|
54
|
+
/** 边框样式 */
|
|
55
|
+
style: string
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** 文字色 */
|
|
59
|
+
'text-color': {
|
|
60
|
+
/** 标题文字颜色 */
|
|
61
|
+
title: string
|
|
62
|
+
/** 主要文字颜色 */
|
|
63
|
+
main: string
|
|
64
|
+
/** 占位符文字颜色 */
|
|
65
|
+
placeholder: string
|
|
66
|
+
/** 次要文字颜色 */
|
|
67
|
+
second: string
|
|
68
|
+
/** 辅助文字颜色 */
|
|
69
|
+
assist: string
|
|
70
|
+
/** 禁用文字颜色 */
|
|
71
|
+
disabled: string
|
|
72
|
+
/** 白色文字 */
|
|
73
|
+
white: string
|
|
74
|
+
}
|
|
75
|
+
/** 圆角大小 */
|
|
76
|
+
radius: {
|
|
77
|
+
/** 小圆角 */
|
|
78
|
+
small: number
|
|
79
|
+
/** 默认圆角 */
|
|
80
|
+
default: number
|
|
81
|
+
/** 大圆角 */
|
|
82
|
+
large: number
|
|
83
|
+
}
|
|
84
|
+
/** 表单组件高度 */
|
|
85
|
+
'form-component-height': {
|
|
86
|
+
/** 小尺寸表单组件高度 */
|
|
87
|
+
small: number
|
|
88
|
+
/** 默认尺寸表单组件高度 */
|
|
89
|
+
default: number
|
|
90
|
+
/** 大尺寸表单组件高度 */
|
|
91
|
+
large: number
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** 字体族 */
|
|
95
|
+
'font-family': string
|
|
96
|
+
/** 标题字体大小 */
|
|
97
|
+
'font-size-title': {
|
|
98
|
+
/** 小尺寸标题字体 */
|
|
99
|
+
small: number
|
|
100
|
+
/** 默认尺寸标题字体 */
|
|
101
|
+
default: number
|
|
102
|
+
/** 大尺寸标题字体 */
|
|
103
|
+
large: number
|
|
104
|
+
}
|
|
105
|
+
/** 正文字体大小 */
|
|
106
|
+
'font-size-main': {
|
|
107
|
+
/** 小尺寸正文字体 */
|
|
108
|
+
small: number
|
|
109
|
+
/** 默认尺寸正文字体 */
|
|
110
|
+
default: number
|
|
111
|
+
/** 大尺寸正文字体 */
|
|
112
|
+
large: number
|
|
113
|
+
}
|
|
114
|
+
/** 辅助文字字体大小 */
|
|
115
|
+
'font-size-assist': {
|
|
116
|
+
/** 小尺寸辅助文字字体 */
|
|
117
|
+
small: number
|
|
118
|
+
/** 默认尺寸辅助文字字体 */
|
|
119
|
+
default: number
|
|
120
|
+
/** 大尺寸辅助文字字体 */
|
|
121
|
+
large: number
|
|
122
|
+
}
|
|
123
|
+
/** 阴影 */
|
|
124
|
+
shadow: {
|
|
125
|
+
/** 阴影颜色 */
|
|
126
|
+
color: string
|
|
127
|
+
/** 阴影水平偏移 */
|
|
128
|
+
x: number
|
|
129
|
+
/** 阴影垂直偏移 */
|
|
130
|
+
y: number
|
|
131
|
+
/** 阴影模糊半径 */
|
|
132
|
+
blur: number
|
|
133
|
+
/** 阴影扩散半径 */
|
|
134
|
+
spread: number
|
|
135
|
+
}
|
|
136
|
+
/** 间距 */
|
|
137
|
+
gap: {
|
|
138
|
+
/** 小间距 */
|
|
139
|
+
small: number
|
|
140
|
+
/** 默认间距 */
|
|
141
|
+
default: number
|
|
142
|
+
/** 大间距 */
|
|
143
|
+
large: number
|
|
144
|
+
}
|
|
145
|
+
/** 断点 */
|
|
146
|
+
breakpoint: {
|
|
147
|
+
/** 超小屏幕断点 */
|
|
148
|
+
xs: number
|
|
149
|
+
/** 小屏幕断点 */
|
|
150
|
+
sm: number
|
|
151
|
+
/** 中等屏幕断点 */
|
|
152
|
+
md: number
|
|
153
|
+
/** 大屏幕断点 */
|
|
154
|
+
lg: number
|
|
155
|
+
}
|
|
156
|
+
}
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
import { isObj, o, str } from '@cat-kit/core'
|
|
2
|
+
import { withUnit } from '@veltra/utils'
|
|
3
|
+
import { reactive, toRaw, watch } from 'vue'
|
|
4
|
+
|
|
5
|
+
import { mixColor } from './helper'
|
|
6
|
+
import type { Theme } from './type'
|
|
7
|
+
|
|
8
|
+
type RecursivePartial<T> = {
|
|
9
|
+
[P in keyof T]?: T[P] extends object ? RecursivePartial<T[P]> : T[P]
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function isDevEnv(): boolean {
|
|
13
|
+
try {
|
|
14
|
+
if (typeof import.meta !== 'undefined') {
|
|
15
|
+
const env = (import.meta as { env?: { DEV?: boolean } }).env
|
|
16
|
+
if (env?.DEV === true) return true
|
|
17
|
+
}
|
|
18
|
+
} catch {
|
|
19
|
+
/* ignore */
|
|
20
|
+
}
|
|
21
|
+
const proc = (globalThis as { process?: { env?: Record<string, string | undefined> } }).process
|
|
22
|
+
return typeof proc !== 'undefined' && proc.env?.NODE_ENV !== 'production'
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export class UITheme {
|
|
26
|
+
static themeID = 'ultra-ui-theme'
|
|
27
|
+
|
|
28
|
+
private static legacyDeprecationWarned = false
|
|
29
|
+
|
|
30
|
+
private static adoptedSheet: CSSStyleSheet | null = null
|
|
31
|
+
|
|
32
|
+
readonly theme: Theme
|
|
33
|
+
|
|
34
|
+
private readonly reactiveEnabled: boolean
|
|
35
|
+
|
|
36
|
+
constructor(theme: Theme, options?: { reactive?: boolean }) {
|
|
37
|
+
this.reactiveEnabled = options?.reactive !== false
|
|
38
|
+
this.theme = reactive(theme) as Theme
|
|
39
|
+
if (this.reactiveEnabled) {
|
|
40
|
+
watch(this.theme, () => this.render(), { deep: true })
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
static setTheme(mode: 'light' | 'dark' | 'auto'): void {
|
|
45
|
+
if (typeof document === 'undefined') return
|
|
46
|
+
const el = document.documentElement
|
|
47
|
+
if (mode === 'auto') {
|
|
48
|
+
delete el.dataset.theme
|
|
49
|
+
} else {
|
|
50
|
+
el.dataset.theme = mode
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
private renderBase(
|
|
55
|
+
theme: Record<string, unknown>,
|
|
56
|
+
themeRules: string[] = [],
|
|
57
|
+
parentKey = '--u'
|
|
58
|
+
): string[] {
|
|
59
|
+
Object.keys(theme).forEach((key) => {
|
|
60
|
+
const value = theme[key]
|
|
61
|
+
const varKey = `${parentKey.startsWith('-') ? parentKey : str(parentKey).kebabCase()}-${str(key).kebabCase()}`
|
|
62
|
+
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
|
|
63
|
+
this.renderBase(value as Record<string, unknown>, themeRules, varKey)
|
|
64
|
+
} else {
|
|
65
|
+
if (value || value === 0) {
|
|
66
|
+
const v = withUnit(value as number | string, 'px')
|
|
67
|
+
if (v !== undefined) {
|
|
68
|
+
themeRules.push(`${varKey}: ${v}`)
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
return themeRules
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
private renderBorder(theme: Theme): string {
|
|
78
|
+
const border = Object.keys(theme.border)
|
|
79
|
+
.map((key) => `var(--u-border-${key})`)
|
|
80
|
+
.join(' ')
|
|
81
|
+
|
|
82
|
+
return `--u-border: ${border}`
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
private renderTypeColor(theme: Theme): string {
|
|
86
|
+
const { color } = theme
|
|
87
|
+
|
|
88
|
+
const types = Object.keys(color)
|
|
89
|
+
const rates = [1, 3, 5, 7, 9]
|
|
90
|
+
|
|
91
|
+
return types
|
|
92
|
+
.map((type) => {
|
|
93
|
+
const colorValue = color[type as keyof typeof color]! as `#${string}`
|
|
94
|
+
|
|
95
|
+
return rates
|
|
96
|
+
.map((rate) => {
|
|
97
|
+
const light = `--u-color-${type}-light-${rate}: ${mixColor(
|
|
98
|
+
colorValue,
|
|
99
|
+
'#fff',
|
|
100
|
+
rate / 10
|
|
101
|
+
)}`
|
|
102
|
+
const dark = `--u-color-${type}-dark-${rate}: ${mixColor(
|
|
103
|
+
colorValue,
|
|
104
|
+
'#000',
|
|
105
|
+
rate / 10
|
|
106
|
+
)}`
|
|
107
|
+
return `${light};${dark}`
|
|
108
|
+
})
|
|
109
|
+
.join(';')
|
|
110
|
+
})
|
|
111
|
+
.join(';')
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
private renderShadow(_theme: Theme): string {
|
|
115
|
+
const shadow = ['x', 'y', 'blur', 'spread', 'color']
|
|
116
|
+
.map((k) => `var(--u-shadow-${k})`)
|
|
117
|
+
.join(' ')
|
|
118
|
+
|
|
119
|
+
return `--u-shadow: ${shadow}`
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
private renderBGColorAlpha(theme: Theme): string {
|
|
123
|
+
const { color } = theme.bg
|
|
124
|
+
return Object.keys(color)
|
|
125
|
+
.map((type) => `--u-bg-color-${type}-alpha: ${color[type as keyof typeof color]}aa`)
|
|
126
|
+
.join(';')
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
private renderBGFilter(theme: Theme): string {
|
|
130
|
+
const { filter } = theme.bg
|
|
131
|
+
return `--u-bg-filter: ${filter.blur} ${filter.saturate}`
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** 主题变量声明(不含 legacy 副本) */
|
|
135
|
+
themeToDeclarationList(theme: Theme): string[] {
|
|
136
|
+
const raw = theme as unknown as Record<string, unknown>
|
|
137
|
+
const lines: string[] = [...this.renderBase(raw)]
|
|
138
|
+
const chunks = [
|
|
139
|
+
this.renderTypeColor(theme),
|
|
140
|
+
this.renderShadow(theme),
|
|
141
|
+
this.renderBGColorAlpha(theme),
|
|
142
|
+
this.renderBGFilter(theme),
|
|
143
|
+
this.renderBorder(theme)
|
|
144
|
+
]
|
|
145
|
+
for (const c of chunks) {
|
|
146
|
+
lines.push(
|
|
147
|
+
...c
|
|
148
|
+
.split(';')
|
|
149
|
+
.map((s) => s.trim())
|
|
150
|
+
.filter(Boolean)
|
|
151
|
+
)
|
|
152
|
+
}
|
|
153
|
+
return lines
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
private static withLegacyDuplicates(decls: string[]): string {
|
|
157
|
+
const legacy: string[] = []
|
|
158
|
+
for (const decl of decls) {
|
|
159
|
+
const idx = decl.indexOf(':')
|
|
160
|
+
if (idx === -1) continue
|
|
161
|
+
const name = decl.slice(0, idx).trim()
|
|
162
|
+
const value = decl.slice(idx + 1).trim()
|
|
163
|
+
if (name.startsWith('--u-')) {
|
|
164
|
+
legacy.push(`--${name.slice(4)}: ${value}`)
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return [...decls, ...legacy].join(';')
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
private static warnLegacyOnce(): void {
|
|
171
|
+
if (!isDevEnv() || UITheme.legacyDeprecationWarned) return
|
|
172
|
+
UITheme.legacyDeprecationWarned = true
|
|
173
|
+
console.warn(
|
|
174
|
+
'[@veltra/styles] Theme CSS variables now prefer the `--u-` namespace. ' +
|
|
175
|
+
'Unprefixed aliases (e.g. `--color-primary`) are deprecated and will be removed in a future major version.'
|
|
176
|
+
)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
private static declarationBlock(decls: string[]): string {
|
|
180
|
+
const merged = UITheme.withLegacyDuplicates(decls)
|
|
181
|
+
UITheme.warnLegacyOnce()
|
|
182
|
+
return merged
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
private static removeExistingStyleTag(doc: Document): void {
|
|
186
|
+
const el = doc.getElementById(UITheme.themeID)
|
|
187
|
+
if (el?.parentNode) {
|
|
188
|
+
el.parentNode.removeChild(el)
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
private static applyGlobalCSS(css: string): void {
|
|
193
|
+
const doc = typeof document !== 'undefined' ? document : undefined
|
|
194
|
+
if (!doc) return
|
|
195
|
+
|
|
196
|
+
const canAdopt =
|
|
197
|
+
typeof CSSStyleSheet !== 'undefined' && 'adoptedStyleSheets' in Document.prototype
|
|
198
|
+
|
|
199
|
+
if (canAdopt) {
|
|
200
|
+
UITheme.removeExistingStyleTag(doc)
|
|
201
|
+
if (!UITheme.adoptedSheet) {
|
|
202
|
+
UITheme.adoptedSheet = new CSSStyleSheet()
|
|
203
|
+
doc.adoptedStyleSheets = [...doc.adoptedStyleSheets, UITheme.adoptedSheet]
|
|
204
|
+
}
|
|
205
|
+
UITheme.adoptedSheet.replaceSync(css)
|
|
206
|
+
} else {
|
|
207
|
+
if (UITheme.adoptedSheet) {
|
|
208
|
+
UITheme.adoptedSheet = null
|
|
209
|
+
}
|
|
210
|
+
let style = doc.getElementById(UITheme.themeID)
|
|
211
|
+
if (!style) {
|
|
212
|
+
style = doc.createElement('style')
|
|
213
|
+
style.id = UITheme.themeID
|
|
214
|
+
doc.head.appendChild(style)
|
|
215
|
+
}
|
|
216
|
+
style.textContent = css
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/** 内置 light/dark:支持 data-theme 与 prefers-color-scheme */
|
|
221
|
+
static injectBuiltInThemes(light: UITheme, dark: UITheme): void {
|
|
222
|
+
const lightDecls = light.themeToDeclarationList(toRaw(light.theme))
|
|
223
|
+
const darkDecls = dark.themeToDeclarationList(toRaw(dark.theme))
|
|
224
|
+
const lightBlock = UITheme.declarationBlock(lightDecls)
|
|
225
|
+
const darkBlock = UITheme.declarationBlock(darkDecls)
|
|
226
|
+
|
|
227
|
+
const css = [
|
|
228
|
+
`html { ${lightBlock} }`,
|
|
229
|
+
`@media (prefers-color-scheme: dark) {`,
|
|
230
|
+
` html:not([data-theme="light"]) { ${darkBlock} }`,
|
|
231
|
+
`}`,
|
|
232
|
+
`html[data-theme="light"] { ${lightBlock} }`,
|
|
233
|
+
`html[data-theme="dark"] { ${darkBlock} }`
|
|
234
|
+
].join('\n')
|
|
235
|
+
|
|
236
|
+
UITheme.applyGlobalCSS(css)
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
render(): void {
|
|
240
|
+
const decls = this.themeToDeclarationList(toRaw(this.theme))
|
|
241
|
+
const block = UITheme.declarationBlock(decls)
|
|
242
|
+
const css = `html { ${block} }`
|
|
243
|
+
UITheme.applyGlobalCSS(css)
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
new(customTheme: RecursivePartial<Theme> = {}): UITheme {
|
|
247
|
+
function delEmpty(obj: Record<string, unknown>) {
|
|
248
|
+
Object.keys(obj).forEach((key) => {
|
|
249
|
+
const value = obj[key]
|
|
250
|
+
if (isObj(value)) {
|
|
251
|
+
return delEmpty(value as Record<string, unknown>)
|
|
252
|
+
}
|
|
253
|
+
if (!value && value !== 0) {
|
|
254
|
+
delete obj[key]
|
|
255
|
+
}
|
|
256
|
+
})
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
delEmpty(customTheme as Record<string, unknown>)
|
|
260
|
+
|
|
261
|
+
const base = JSON.parse(JSON.stringify(toRaw(this.theme))) as Record<string, any>
|
|
262
|
+
o(base).deepExtend(customTheme as Record<string, any>)
|
|
263
|
+
return new UITheme(base as Theme, { reactive: this.reactiveEnabled })
|
|
264
|
+
}
|
|
265
|
+
}
|