@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.
Files changed (52) hide show
  1. package/dist/_functions.scss +49 -0
  2. package/dist/_mixins.scss +177 -0
  3. package/dist/_vars.scss +44 -0
  4. package/dist/anime/fade.css +9 -0
  5. package/dist/anime/fade.scss +8 -0
  6. package/dist/anime/slide.css +18 -0
  7. package/dist/anime/slide.scss +28 -0
  8. package/dist/anime/spring.css +30 -0
  9. package/dist/anime/spring.scss +42 -0
  10. package/dist/anime/zoom-in.css +57 -0
  11. package/dist/anime/zoom-in.scss +73 -0
  12. package/dist/index.d.ts +1 -0
  13. package/dist/index.js +5 -0
  14. package/dist/load-theme.d.ts +18 -0
  15. package/dist/load-theme.js +38 -0
  16. package/dist/load-theme.js.map +1 -0
  17. package/dist/normalize.css +76 -0
  18. package/dist/normalize.scss +63 -0
  19. package/dist/theme/dark.d.ts +7 -0
  20. package/dist/theme/dark.js +47 -0
  21. package/dist/theme/dark.js.map +1 -0
  22. package/dist/theme/helper.d.ts +22 -0
  23. package/dist/theme/helper.js +57 -0
  24. package/dist/theme/helper.js.map +1 -0
  25. package/dist/theme/index.d.ts +7 -0
  26. package/dist/theme/index.js +6 -0
  27. package/dist/theme/light.d.ts +7 -0
  28. package/dist/theme/light.js +89 -0
  29. package/dist/theme/light.js.map +1 -0
  30. package/dist/theme/type.d.ts +91 -0
  31. package/dist/theme/ui-theme.d.ts +35 -0
  32. package/dist/theme/ui-theme.js +179 -0
  33. package/dist/theme/ui-theme.js.map +1 -0
  34. package/package.json +82 -0
  35. package/src/_functions.scss +49 -0
  36. package/src/_mixins.scss +177 -0
  37. package/src/_vars.scss +44 -0
  38. package/src/anime/fade.scss +8 -0
  39. package/src/anime/slide.scss +28 -0
  40. package/src/anime/spring.scss +42 -0
  41. package/src/anime/zoom-in.scss +73 -0
  42. package/src/env.d.ts +1 -0
  43. package/src/index.ts +5 -0
  44. package/src/load-theme.ts +46 -0
  45. package/src/normalize.scss +63 -0
  46. package/src/theme/__test__/ui-theme.test.ts +27 -0
  47. package/src/theme/dark.ts +38 -0
  48. package/src/theme/helper.ts +58 -0
  49. package/src/theme/index.ts +6 -0
  50. package/src/theme/light.ts +60 -0
  51. package/src/theme/type.ts +156 -0
  52. 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
+ }