@yh-ui/theme 0.1.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/LICENSE +21 -0
- package/dist/component-theme.cjs +69 -0
- package/dist/component-theme.d.ts +269 -0
- package/dist/component-theme.mjs +54 -0
- package/dist/index.cjs +38 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.mjs +3 -0
- package/dist/styles/components.css +589 -0
- package/dist/styles/index.css +584 -0
- package/dist/styles/mixins/mixins.css +451 -0
- package/dist/styles/reset.css +129 -0
- package/dist/styles/variables.css +451 -0
- package/dist/theme.cjs +1261 -0
- package/dist/theme.d.ts +313 -0
- package/dist/theme.mjs +1200 -0
- package/dist/tokens/index.cjs +244 -0
- package/dist/tokens/index.d.ts +458 -0
- package/dist/tokens/index.mjs +238 -0
- package/package.json +52 -0
- package/src/styles/components.scss +9 -0
- package/src/styles/index.scss +8 -0
- package/src/styles/mixins/mixins.scss +96 -0
- package/src/styles/reset.scss +131 -0
- package/src/styles/variables.scss +513 -0
package/dist/theme.mjs
ADDED
|
@@ -0,0 +1,1200 @@
|
|
|
1
|
+
import { reactive, toRefs } from "vue";
|
|
2
|
+
import { designTokens } from "./tokens/index.mjs";
|
|
3
|
+
const presetThemes = {
|
|
4
|
+
default: {
|
|
5
|
+
primary: "#409eff",
|
|
6
|
+
success: "#67c23a",
|
|
7
|
+
warning: "#e6a23c",
|
|
8
|
+
danger: "#f56c6c",
|
|
9
|
+
info: "#909399"
|
|
10
|
+
},
|
|
11
|
+
dark: {
|
|
12
|
+
primary: "#409eff",
|
|
13
|
+
success: "#67c23a",
|
|
14
|
+
warning: "#e6a23c",
|
|
15
|
+
danger: "#f56c6c",
|
|
16
|
+
info: "#909399"
|
|
17
|
+
},
|
|
18
|
+
blue: {
|
|
19
|
+
primary: "#1890ff",
|
|
20
|
+
success: "#52c41a",
|
|
21
|
+
warning: "#faad14",
|
|
22
|
+
danger: "#ff4d4f",
|
|
23
|
+
info: "#909399"
|
|
24
|
+
},
|
|
25
|
+
green: {
|
|
26
|
+
primary: "#10b981",
|
|
27
|
+
success: "#22c55e",
|
|
28
|
+
warning: "#f59e0b",
|
|
29
|
+
danger: "#ef4444",
|
|
30
|
+
info: "#6b7280"
|
|
31
|
+
},
|
|
32
|
+
purple: {
|
|
33
|
+
primary: "#8b5cf6",
|
|
34
|
+
success: "#22c55e",
|
|
35
|
+
warning: "#f59e0b",
|
|
36
|
+
danger: "#ef4444",
|
|
37
|
+
info: "#6b7280"
|
|
38
|
+
},
|
|
39
|
+
orange: {
|
|
40
|
+
primary: "#f97316",
|
|
41
|
+
success: "#22c55e",
|
|
42
|
+
warning: "#eab308",
|
|
43
|
+
danger: "#ef4444",
|
|
44
|
+
info: "#6b7280"
|
|
45
|
+
},
|
|
46
|
+
rose: {
|
|
47
|
+
primary: "#f43f5e",
|
|
48
|
+
success: "#22c55e",
|
|
49
|
+
warning: "#f59e0b",
|
|
50
|
+
danger: "#dc2626",
|
|
51
|
+
info: "#6b7280"
|
|
52
|
+
},
|
|
53
|
+
amber: {
|
|
54
|
+
primary: "#f59e0b",
|
|
55
|
+
success: "#22c55e",
|
|
56
|
+
warning: "#eab308",
|
|
57
|
+
danger: "#ef4444",
|
|
58
|
+
info: "#6b7280"
|
|
59
|
+
},
|
|
60
|
+
teal: {
|
|
61
|
+
primary: "#14b8a6",
|
|
62
|
+
success: "#22c55e",
|
|
63
|
+
warning: "#f59e0b",
|
|
64
|
+
danger: "#ef4444",
|
|
65
|
+
info: "#6b7280"
|
|
66
|
+
},
|
|
67
|
+
indigo: {
|
|
68
|
+
primary: "#6366f1",
|
|
69
|
+
success: "#22c55e",
|
|
70
|
+
warning: "#f59e0b",
|
|
71
|
+
danger: "#ef4444",
|
|
72
|
+
info: "#6b7280"
|
|
73
|
+
},
|
|
74
|
+
slate: {
|
|
75
|
+
primary: "#475569",
|
|
76
|
+
success: "#22c55e",
|
|
77
|
+
warning: "#f59e0b",
|
|
78
|
+
danger: "#ef4444",
|
|
79
|
+
info: "#64748b"
|
|
80
|
+
},
|
|
81
|
+
zinc: {
|
|
82
|
+
primary: "#52525b",
|
|
83
|
+
success: "#22c55e",
|
|
84
|
+
warning: "#f59e0b",
|
|
85
|
+
danger: "#ef4444",
|
|
86
|
+
info: "#71717a"
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
function hexToRgb(hex) {
|
|
90
|
+
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
|
|
91
|
+
return result ? {
|
|
92
|
+
r: parseInt(result[1], 16),
|
|
93
|
+
g: parseInt(result[2], 16),
|
|
94
|
+
b: parseInt(result[3], 16)
|
|
95
|
+
} : null;
|
|
96
|
+
}
|
|
97
|
+
function rgbToHex(r, g, b) {
|
|
98
|
+
return `#${((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1)}`;
|
|
99
|
+
}
|
|
100
|
+
function rgbToHsl(r, g, b) {
|
|
101
|
+
r /= 255;
|
|
102
|
+
g /= 255;
|
|
103
|
+
b /= 255;
|
|
104
|
+
const max = Math.max(r, g, b);
|
|
105
|
+
const min = Math.min(r, g, b);
|
|
106
|
+
let h = 0;
|
|
107
|
+
let s = 0;
|
|
108
|
+
const l = (max + min) / 2;
|
|
109
|
+
if (max !== min) {
|
|
110
|
+
const d = max - min;
|
|
111
|
+
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
|
|
112
|
+
switch (max) {
|
|
113
|
+
case r:
|
|
114
|
+
h = ((g - b) / d + (g < b ? 6 : 0)) / 6;
|
|
115
|
+
break;
|
|
116
|
+
case g:
|
|
117
|
+
h = ((b - r) / d + 2) / 6;
|
|
118
|
+
break;
|
|
119
|
+
case b:
|
|
120
|
+
h = ((r - g) / d + 4) / 6;
|
|
121
|
+
break;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return { h: h * 360, s: s * 100, l: l * 100 };
|
|
125
|
+
}
|
|
126
|
+
function hslToRgb(h, s, l) {
|
|
127
|
+
h /= 360;
|
|
128
|
+
s /= 100;
|
|
129
|
+
l /= 100;
|
|
130
|
+
let r, g, b;
|
|
131
|
+
if (s === 0) {
|
|
132
|
+
r = g = b = l;
|
|
133
|
+
} else {
|
|
134
|
+
const hue2rgb = (p2, q2, t) => {
|
|
135
|
+
if (t < 0) t += 1;
|
|
136
|
+
if (t > 1) t -= 1;
|
|
137
|
+
if (t < 1 / 6) return p2 + (q2 - p2) * 6 * t;
|
|
138
|
+
if (t < 1 / 2) return q2;
|
|
139
|
+
if (t < 2 / 3) return p2 + (q2 - p2) * (2 / 3 - t) * 6;
|
|
140
|
+
return p2;
|
|
141
|
+
};
|
|
142
|
+
const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
|
|
143
|
+
const p = 2 * l - q;
|
|
144
|
+
r = hue2rgb(p, q, h + 1 / 3);
|
|
145
|
+
g = hue2rgb(p, q, h);
|
|
146
|
+
b = hue2rgb(p, q, h - 1 / 3);
|
|
147
|
+
}
|
|
148
|
+
return {
|
|
149
|
+
r: Math.round(r * 255),
|
|
150
|
+
g: Math.round(g * 255),
|
|
151
|
+
b: Math.round(b * 255)
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
function getRelativeLuminance(r, g, b) {
|
|
155
|
+
const [rs, gs, bs] = [r, g, b].map((c) => {
|
|
156
|
+
c /= 255;
|
|
157
|
+
return c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
|
|
158
|
+
});
|
|
159
|
+
return 0.2126 * rs + 0.7152 * gs + 0.0722 * bs;
|
|
160
|
+
}
|
|
161
|
+
function getContrastRatio(color1, color2) {
|
|
162
|
+
const rgb1 = hexToRgb(color1);
|
|
163
|
+
const rgb2 = hexToRgb(color2);
|
|
164
|
+
if (!rgb1 || !rgb2) return 1;
|
|
165
|
+
const l1 = getRelativeLuminance(rgb1.r, rgb1.g, rgb1.b);
|
|
166
|
+
const l2 = getRelativeLuminance(rgb2.r, rgb2.g, rgb2.b);
|
|
167
|
+
const lighter = Math.max(l1, l2);
|
|
168
|
+
const darker = Math.min(l1, l2);
|
|
169
|
+
return (lighter + 0.05) / (darker + 0.05);
|
|
170
|
+
}
|
|
171
|
+
function ensureContrast(foreground, background, minRatio = 4.5) {
|
|
172
|
+
const ratio = getContrastRatio(foreground, background);
|
|
173
|
+
if (ratio >= minRatio) return foreground;
|
|
174
|
+
const fgRgb = hexToRgb(foreground);
|
|
175
|
+
if (!fgRgb) return foreground;
|
|
176
|
+
const hsl = rgbToHsl(fgRgb.r, fgRgb.g, fgRgb.b);
|
|
177
|
+
const bgRgb = hexToRgb(background);
|
|
178
|
+
if (!bgRgb) return foreground;
|
|
179
|
+
const bgLuminance = getRelativeLuminance(bgRgb.r, bgRgb.g, bgRgb.b);
|
|
180
|
+
if (bgLuminance > 0.5) {
|
|
181
|
+
while (hsl.l > 0 && getContrastRatio(
|
|
182
|
+
rgbToHex(...Object.values(hslToRgb(hsl.h, hsl.s, hsl.l))),
|
|
183
|
+
background
|
|
184
|
+
) < minRatio) {
|
|
185
|
+
hsl.l -= 5;
|
|
186
|
+
}
|
|
187
|
+
} else {
|
|
188
|
+
while (hsl.l < 100 && getContrastRatio(
|
|
189
|
+
rgbToHex(...Object.values(hslToRgb(hsl.h, hsl.s, hsl.l))),
|
|
190
|
+
background
|
|
191
|
+
) < minRatio) {
|
|
192
|
+
hsl.l += 5;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
const adjusted = hslToRgb(hsl.h, hsl.s, hsl.l);
|
|
196
|
+
return rgbToHex(adjusted.r, adjusted.g, adjusted.b);
|
|
197
|
+
}
|
|
198
|
+
function mixColor(color1, color2, weight) {
|
|
199
|
+
const rgb1 = hexToRgb(color1);
|
|
200
|
+
const rgb2 = hexToRgb(color2);
|
|
201
|
+
if (!rgb1 || !rgb2) return color1;
|
|
202
|
+
const w = weight / 100;
|
|
203
|
+
const r = Math.round(rgb1.r * w + rgb2.r * (1 - w));
|
|
204
|
+
const g = Math.round(rgb1.g * w + rgb2.g * (1 - w));
|
|
205
|
+
const b = Math.round(rgb1.b * w + rgb2.b * (1 - w));
|
|
206
|
+
return rgbToHex(r, g, b);
|
|
207
|
+
}
|
|
208
|
+
function adjustSaturation(color, amount) {
|
|
209
|
+
const rgb = hexToRgb(color);
|
|
210
|
+
if (!rgb) return color;
|
|
211
|
+
const hsl = rgbToHsl(rgb.r, rgb.g, rgb.b);
|
|
212
|
+
hsl.s = Math.max(0, Math.min(100, hsl.s + amount));
|
|
213
|
+
const adjusted = hslToRgb(hsl.h, hsl.s, hsl.l);
|
|
214
|
+
return rgbToHex(adjusted.r, adjusted.g, adjusted.b);
|
|
215
|
+
}
|
|
216
|
+
function adjustLightness(color, amount) {
|
|
217
|
+
const rgb = hexToRgb(color);
|
|
218
|
+
if (!rgb) return color;
|
|
219
|
+
const hsl = rgbToHsl(rgb.r, rgb.g, rgb.b);
|
|
220
|
+
hsl.l = Math.max(0, Math.min(100, hsl.l + amount));
|
|
221
|
+
const adjusted = hslToRgb(hsl.h, hsl.s, hsl.l);
|
|
222
|
+
return rgbToHex(adjusted.r, adjusted.g, adjusted.b);
|
|
223
|
+
}
|
|
224
|
+
function generateColorScaleWithAlgorithm(baseColor, isDark = false, algorithm = "default") {
|
|
225
|
+
const white = "#ffffff";
|
|
226
|
+
const black = "#000000";
|
|
227
|
+
let adjustedBase = baseColor;
|
|
228
|
+
switch (algorithm) {
|
|
229
|
+
case "vibrant":
|
|
230
|
+
adjustedBase = adjustSaturation(baseColor, 15);
|
|
231
|
+
break;
|
|
232
|
+
case "muted":
|
|
233
|
+
adjustedBase = adjustSaturation(baseColor, -20);
|
|
234
|
+
break;
|
|
235
|
+
case "pastel":
|
|
236
|
+
adjustedBase = adjustSaturation(adjustLightness(baseColor, 15), -30);
|
|
237
|
+
break;
|
|
238
|
+
}
|
|
239
|
+
if (isDark) {
|
|
240
|
+
return {
|
|
241
|
+
"": adjustedBase,
|
|
242
|
+
"light-3": mixColor(adjustedBase, black, 70),
|
|
243
|
+
"light-5": mixColor(adjustedBase, black, 50),
|
|
244
|
+
"light-7": mixColor(adjustedBase, black, 30),
|
|
245
|
+
"light-8": mixColor(adjustedBase, black, 20),
|
|
246
|
+
"light-9": mixColor(adjustedBase, black, 10),
|
|
247
|
+
"dark-2": mixColor(adjustedBase, white, 80)
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
return {
|
|
251
|
+
"": adjustedBase,
|
|
252
|
+
"light-1": mixColor(adjustedBase, white, 90),
|
|
253
|
+
"light-2": mixColor(adjustedBase, white, 80),
|
|
254
|
+
"light-3": mixColor(adjustedBase, white, 70),
|
|
255
|
+
"light-4": mixColor(adjustedBase, white, 60),
|
|
256
|
+
"light-5": mixColor(adjustedBase, white, 50),
|
|
257
|
+
"light-6": mixColor(adjustedBase, white, 40),
|
|
258
|
+
"light-7": mixColor(adjustedBase, white, 30),
|
|
259
|
+
"light-8": mixColor(adjustedBase, white, 20),
|
|
260
|
+
"light-9": mixColor(adjustedBase, white, 10),
|
|
261
|
+
"dark-2": mixColor(adjustedBase, black, 80)
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
function generateSemanticColors(baseColor, isDark = false) {
|
|
265
|
+
return {
|
|
266
|
+
hover: isDark ? adjustLightness(baseColor, 10) : adjustLightness(baseColor, -5),
|
|
267
|
+
active: isDark ? adjustLightness(baseColor, -5) : adjustLightness(baseColor, -10),
|
|
268
|
+
disabled: adjustSaturation(adjustLightness(baseColor, isDark ? -20 : 30), -40),
|
|
269
|
+
focus: baseColor
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
function setCssVar(name, value, el = null) {
|
|
273
|
+
const target = el || (typeof document !== "undefined" ? document.documentElement : null);
|
|
274
|
+
if (target) {
|
|
275
|
+
target.style.setProperty(name, value);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
function getCssVar(name, el = null) {
|
|
279
|
+
const target = el || (typeof document !== "undefined" ? document.documentElement : null);
|
|
280
|
+
if (target) {
|
|
281
|
+
return getComputedStyle(target).getPropertyValue(name).trim();
|
|
282
|
+
}
|
|
283
|
+
return "";
|
|
284
|
+
}
|
|
285
|
+
function removeCssVar(name, el = null) {
|
|
286
|
+
const target = el || (typeof document !== "undefined" ? document.documentElement : null);
|
|
287
|
+
if (target) {
|
|
288
|
+
target.style.removeProperty(name);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
function getTargetElement(scope) {
|
|
292
|
+
if (typeof document === "undefined") return null;
|
|
293
|
+
if (!scope) return document.documentElement;
|
|
294
|
+
if (typeof scope === "string") {
|
|
295
|
+
return document.querySelector(scope);
|
|
296
|
+
}
|
|
297
|
+
return scope;
|
|
298
|
+
}
|
|
299
|
+
const breakpoints = {
|
|
300
|
+
xs: 0,
|
|
301
|
+
sm: 576,
|
|
302
|
+
md: 768,
|
|
303
|
+
lg: 992,
|
|
304
|
+
xl: 1200,
|
|
305
|
+
xxl: 1400
|
|
306
|
+
};
|
|
307
|
+
const densityConfig = {
|
|
308
|
+
comfortable: {
|
|
309
|
+
"--yh-density-factor": "1",
|
|
310
|
+
"--yh-component-size-default": "32px",
|
|
311
|
+
"--yh-component-size-small": "24px",
|
|
312
|
+
"--yh-component-size-large": "40px",
|
|
313
|
+
"--yh-padding-default": "12px 16px",
|
|
314
|
+
"--yh-padding-small": "8px 12px",
|
|
315
|
+
"--yh-padding-large": "16px 20px",
|
|
316
|
+
"--yh-spacing-unit": "8px",
|
|
317
|
+
"--yh-font-size-base": "14px",
|
|
318
|
+
"--yh-line-height-base": "1.5"
|
|
319
|
+
},
|
|
320
|
+
compact: {
|
|
321
|
+
"--yh-density-factor": "0.85",
|
|
322
|
+
"--yh-component-size-default": "28px",
|
|
323
|
+
"--yh-component-size-small": "20px",
|
|
324
|
+
"--yh-component-size-large": "36px",
|
|
325
|
+
"--yh-padding-default": "8px 12px",
|
|
326
|
+
"--yh-padding-small": "4px 8px",
|
|
327
|
+
"--yh-padding-large": "12px 16px",
|
|
328
|
+
"--yh-spacing-unit": "6px",
|
|
329
|
+
"--yh-font-size-base": "13px",
|
|
330
|
+
"--yh-line-height-base": "1.4"
|
|
331
|
+
},
|
|
332
|
+
dense: {
|
|
333
|
+
"--yh-density-factor": "0.7",
|
|
334
|
+
"--yh-component-size-default": "24px",
|
|
335
|
+
"--yh-component-size-small": "18px",
|
|
336
|
+
"--yh-component-size-large": "32px",
|
|
337
|
+
"--yh-padding-default": "4px 8px",
|
|
338
|
+
"--yh-padding-small": "2px 6px",
|
|
339
|
+
"--yh-padding-large": "8px 12px",
|
|
340
|
+
"--yh-spacing-unit": "4px",
|
|
341
|
+
"--yh-font-size-base": "12px",
|
|
342
|
+
"--yh-line-height-base": "1.35"
|
|
343
|
+
}
|
|
344
|
+
};
|
|
345
|
+
const colorBlindPalettes = {
|
|
346
|
+
none: {},
|
|
347
|
+
// 红色盲(无法区分红绿)
|
|
348
|
+
protanopia: {
|
|
349
|
+
primary: "#0072B2",
|
|
350
|
+
// 蓝色
|
|
351
|
+
success: "#009E73",
|
|
352
|
+
// 蓝绿色
|
|
353
|
+
warning: "#E69F00",
|
|
354
|
+
// 橙色
|
|
355
|
+
danger: "#D55E00",
|
|
356
|
+
// 深橙色
|
|
357
|
+
info: "#56B4E9"
|
|
358
|
+
// 浅蓝色
|
|
359
|
+
},
|
|
360
|
+
// 绿色盲(无法区分红绿)
|
|
361
|
+
deuteranopia: {
|
|
362
|
+
primary: "#0072B2",
|
|
363
|
+
success: "#009E73",
|
|
364
|
+
warning: "#E69F00",
|
|
365
|
+
danger: "#CC79A7",
|
|
366
|
+
// 粉紫色
|
|
367
|
+
info: "#56B4E9"
|
|
368
|
+
},
|
|
369
|
+
// 蓝色盲(无法区分蓝黄)
|
|
370
|
+
tritanopia: {
|
|
371
|
+
primary: "#CC79A7",
|
|
372
|
+
// 粉紫色
|
|
373
|
+
success: "#009E73",
|
|
374
|
+
// 蓝绿色
|
|
375
|
+
warning: "#D55E00",
|
|
376
|
+
// 深橙色
|
|
377
|
+
danger: "#E69F00",
|
|
378
|
+
// 橙色
|
|
379
|
+
info: "#999999"
|
|
380
|
+
// 灰色
|
|
381
|
+
},
|
|
382
|
+
// 全色盲(只能看到灰度)
|
|
383
|
+
achromatopsia: {
|
|
384
|
+
primary: "#404040",
|
|
385
|
+
success: "#606060",
|
|
386
|
+
warning: "#808080",
|
|
387
|
+
danger: "#202020",
|
|
388
|
+
info: "#a0a0a0"
|
|
389
|
+
}
|
|
390
|
+
};
|
|
391
|
+
const THEME_TRANSITION_CLASS = "yh-theme-transitioning";
|
|
392
|
+
const DEFAULT_TRANSITION_DURATION = 300;
|
|
393
|
+
const DEFAULT_TRANSITION_TIMING = "cubic-bezier(0.4, 0, 0.2, 1)";
|
|
394
|
+
function enableThemeTransition(duration = DEFAULT_TRANSITION_DURATION, timing = DEFAULT_TRANSITION_TIMING) {
|
|
395
|
+
if (typeof document === "undefined") return;
|
|
396
|
+
const style = document.createElement("style");
|
|
397
|
+
style.id = "yh-theme-transition";
|
|
398
|
+
style.textContent = `
|
|
399
|
+
.${THEME_TRANSITION_CLASS},
|
|
400
|
+
.${THEME_TRANSITION_CLASS} *,
|
|
401
|
+
.${THEME_TRANSITION_CLASS} *::before,
|
|
402
|
+
.${THEME_TRANSITION_CLASS} *::after {
|
|
403
|
+
transition:
|
|
404
|
+
background-color ${duration}ms ${timing},
|
|
405
|
+
border-color ${duration}ms ${timing},
|
|
406
|
+
color ${duration}ms ${timing},
|
|
407
|
+
fill ${duration}ms ${timing},
|
|
408
|
+
stroke ${duration}ms ${timing},
|
|
409
|
+
box-shadow ${duration}ms ${timing} !important;
|
|
410
|
+
}
|
|
411
|
+
`;
|
|
412
|
+
document.head.appendChild(style);
|
|
413
|
+
document.documentElement.classList.add(THEME_TRANSITION_CLASS);
|
|
414
|
+
setTimeout(() => {
|
|
415
|
+
document.documentElement.classList.remove(THEME_TRANSITION_CLASS);
|
|
416
|
+
style.remove();
|
|
417
|
+
}, duration);
|
|
418
|
+
}
|
|
419
|
+
function getComplementaryColor(hex) {
|
|
420
|
+
const rgb = hexToRgb(hex);
|
|
421
|
+
if (!rgb) return hex;
|
|
422
|
+
const hsl = rgbToHsl(rgb.r, rgb.g, rgb.b);
|
|
423
|
+
hsl.h = (hsl.h + 180) % 360;
|
|
424
|
+
const result = hslToRgb(hsl.h, hsl.s, hsl.l);
|
|
425
|
+
return rgbToHex(result.r, result.g, result.b);
|
|
426
|
+
}
|
|
427
|
+
function getAnalogousColors(hex) {
|
|
428
|
+
const rgb = hexToRgb(hex);
|
|
429
|
+
if (!rgb) return [hex, hex];
|
|
430
|
+
const hsl = rgbToHsl(rgb.r, rgb.g, rgb.b);
|
|
431
|
+
const hsl1 = { ...hsl, h: (hsl.h + 30) % 360 };
|
|
432
|
+
const hsl2 = { ...hsl, h: (hsl.h - 30 + 360) % 360 };
|
|
433
|
+
const result1 = hslToRgb(hsl1.h, hsl1.s, hsl1.l);
|
|
434
|
+
const result2 = hslToRgb(hsl2.h, hsl2.s, hsl2.l);
|
|
435
|
+
return [rgbToHex(result1.r, result1.g, result1.b), rgbToHex(result2.r, result2.g, result2.b)];
|
|
436
|
+
}
|
|
437
|
+
function getTriadicColors(hex) {
|
|
438
|
+
const rgb = hexToRgb(hex);
|
|
439
|
+
if (!rgb) return [hex, hex];
|
|
440
|
+
const hsl = rgbToHsl(rgb.r, rgb.g, rgb.b);
|
|
441
|
+
const hsl1 = { ...hsl, h: (hsl.h + 120) % 360 };
|
|
442
|
+
const hsl2 = { ...hsl, h: (hsl.h + 240) % 360 };
|
|
443
|
+
const result1 = hslToRgb(hsl1.h, hsl1.s, hsl1.l);
|
|
444
|
+
const result2 = hslToRgb(hsl2.h, hsl2.s, hsl2.l);
|
|
445
|
+
return [rgbToHex(result1.r, result1.g, result1.b), rgbToHex(result2.r, result2.g, result2.b)];
|
|
446
|
+
}
|
|
447
|
+
function generatePaletteFromPrimary(primaryColor) {
|
|
448
|
+
const rgb = hexToRgb(primaryColor);
|
|
449
|
+
if (!rgb) return { primary: primaryColor };
|
|
450
|
+
const hsl = rgbToHsl(rgb.r, rgb.g, rgb.b);
|
|
451
|
+
const successHsl = { h: 142, s: Math.min(hsl.s, 70), l: 45 };
|
|
452
|
+
const successRgb = hslToRgb(successHsl.h, successHsl.s, successHsl.l);
|
|
453
|
+
const warningHsl = { h: 36, s: Math.min(hsl.s + 10, 85), l: 50 };
|
|
454
|
+
const warningRgb = hslToRgb(warningHsl.h, warningHsl.s, warningHsl.l);
|
|
455
|
+
const dangerHsl = { h: 0, s: Math.min(hsl.s + 5, 75), l: 55 };
|
|
456
|
+
const dangerRgb = hslToRgb(dangerHsl.h, dangerHsl.s, dangerHsl.l);
|
|
457
|
+
const infoHsl = { h: hsl.h, s: Math.max(hsl.s - 40, 10), l: 60 };
|
|
458
|
+
const infoRgb = hslToRgb(infoHsl.h, infoHsl.s, infoHsl.l);
|
|
459
|
+
return {
|
|
460
|
+
primary: primaryColor,
|
|
461
|
+
success: rgbToHex(successRgb.r, successRgb.g, successRgb.b),
|
|
462
|
+
warning: rgbToHex(warningRgb.r, warningRgb.g, warningRgb.b),
|
|
463
|
+
danger: rgbToHex(dangerRgb.r, dangerRgb.g, dangerRgb.b),
|
|
464
|
+
info: rgbToHex(infoRgb.r, infoRgb.g, infoRgb.b)
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
function getCurrentBreakpoint() {
|
|
468
|
+
if (typeof window === "undefined") return "md";
|
|
469
|
+
const width = window.innerWidth;
|
|
470
|
+
if (width >= breakpoints.xxl) return "xxl";
|
|
471
|
+
if (width >= breakpoints.xl) return "xl";
|
|
472
|
+
if (width >= breakpoints.lg) return "lg";
|
|
473
|
+
if (width >= breakpoints.md) return "md";
|
|
474
|
+
if (width >= breakpoints.sm) return "sm";
|
|
475
|
+
return "xs";
|
|
476
|
+
}
|
|
477
|
+
export class ThemeManager {
|
|
478
|
+
currentTheme = "default";
|
|
479
|
+
customColors = {};
|
|
480
|
+
isDark = false;
|
|
481
|
+
targetEl = null;
|
|
482
|
+
persistKey = "yh-ui-theme";
|
|
483
|
+
algorithm = "default";
|
|
484
|
+
responsiveConfig = {};
|
|
485
|
+
currentBreakpoint = "md";
|
|
486
|
+
resizeHandler = null;
|
|
487
|
+
systemDarkQuery = null;
|
|
488
|
+
systemDarkHandler = null;
|
|
489
|
+
followSystem = false;
|
|
490
|
+
currentDensity = "comfortable";
|
|
491
|
+
colorBlindMode = "none";
|
|
492
|
+
componentOverrides = {};
|
|
493
|
+
transitionEnabled = false;
|
|
494
|
+
transitionConfig = {
|
|
495
|
+
duration: DEFAULT_TRANSITION_DURATION,
|
|
496
|
+
timing: DEFAULT_TRANSITION_TIMING
|
|
497
|
+
};
|
|
498
|
+
themeHistory = [];
|
|
499
|
+
maxHistoryLength = 10;
|
|
500
|
+
// 响应式状态
|
|
501
|
+
state = reactive({
|
|
502
|
+
theme: "default",
|
|
503
|
+
dark: false,
|
|
504
|
+
colors: {},
|
|
505
|
+
breakpoint: "md",
|
|
506
|
+
density: "comfortable",
|
|
507
|
+
colorBlindMode: "none"
|
|
508
|
+
});
|
|
509
|
+
constructor(options) {
|
|
510
|
+
this.initTheme(options);
|
|
511
|
+
}
|
|
512
|
+
/** 初始化主题 */
|
|
513
|
+
initTheme(options) {
|
|
514
|
+
if (options?.persist !== false) {
|
|
515
|
+
this.persistKey = options?.persistKey || "yh-ui-theme";
|
|
516
|
+
this.restoreFromStorage();
|
|
517
|
+
}
|
|
518
|
+
this.apply({
|
|
519
|
+
preset: "default",
|
|
520
|
+
...options
|
|
521
|
+
});
|
|
522
|
+
if (options?.responsive) {
|
|
523
|
+
this.setResponsiveTheme(options.responsive);
|
|
524
|
+
}
|
|
525
|
+
if (options?.followSystem) {
|
|
526
|
+
this.enableSystemFollow();
|
|
527
|
+
}
|
|
528
|
+
this.applyTokens();
|
|
529
|
+
}
|
|
530
|
+
/** 应用主题 */
|
|
531
|
+
apply(options) {
|
|
532
|
+
const { preset, colors, dark, scope, algorithm } = options;
|
|
533
|
+
this.targetEl = getTargetElement(scope);
|
|
534
|
+
if (algorithm) {
|
|
535
|
+
this.algorithm = algorithm;
|
|
536
|
+
}
|
|
537
|
+
if (dark !== void 0) {
|
|
538
|
+
this.setDarkMode(dark);
|
|
539
|
+
}
|
|
540
|
+
if (preset) {
|
|
541
|
+
this.setPreset(preset);
|
|
542
|
+
}
|
|
543
|
+
if (colors) {
|
|
544
|
+
this.setColors(colors);
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
/** 设置预设主题 */
|
|
548
|
+
setPreset(preset) {
|
|
549
|
+
const colors = presetThemes[preset];
|
|
550
|
+
if (!colors) {
|
|
551
|
+
console.warn(`[YH-UI Theme] Invalid preset: "${preset}"`);
|
|
552
|
+
return;
|
|
553
|
+
}
|
|
554
|
+
this.currentTheme = preset;
|
|
555
|
+
this.state.theme = preset;
|
|
556
|
+
this.applyColors(colors);
|
|
557
|
+
this.saveToStorage();
|
|
558
|
+
}
|
|
559
|
+
/** 设置自定义颜色 */
|
|
560
|
+
setColors(colors) {
|
|
561
|
+
this.customColors = { ...this.customColors, ...colors };
|
|
562
|
+
this.state.colors = this.customColors;
|
|
563
|
+
this.applyColors(colors);
|
|
564
|
+
this.saveToStorage();
|
|
565
|
+
}
|
|
566
|
+
/** 设置主色 */
|
|
567
|
+
setPrimaryColor(color) {
|
|
568
|
+
this.setColors({ primary: color });
|
|
569
|
+
}
|
|
570
|
+
/** 设置主题色 (别名) */
|
|
571
|
+
setThemeColor(color) {
|
|
572
|
+
this.setPrimaryColor(color);
|
|
573
|
+
}
|
|
574
|
+
/** 设置预设主题 (别名) */
|
|
575
|
+
setThemePreset(preset) {
|
|
576
|
+
this.setPreset(preset);
|
|
577
|
+
}
|
|
578
|
+
/** 设置颜色算法 */
|
|
579
|
+
setAlgorithm(algorithm) {
|
|
580
|
+
this.algorithm = algorithm;
|
|
581
|
+
const currentColors = {
|
|
582
|
+
...presetThemes[this.currentTheme],
|
|
583
|
+
...this.customColors
|
|
584
|
+
};
|
|
585
|
+
this.applyColors(currentColors);
|
|
586
|
+
this.saveToStorage();
|
|
587
|
+
}
|
|
588
|
+
/** 设置暗色模式 */
|
|
589
|
+
setDarkMode(dark) {
|
|
590
|
+
this.isDark = dark;
|
|
591
|
+
this.state.dark = dark;
|
|
592
|
+
if (typeof document !== "undefined") {
|
|
593
|
+
if (dark) {
|
|
594
|
+
document.documentElement.classList.add("dark");
|
|
595
|
+
} else {
|
|
596
|
+
document.documentElement.classList.remove("dark");
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
const currentColors = {
|
|
600
|
+
...presetThemes[this.currentTheme],
|
|
601
|
+
...this.customColors
|
|
602
|
+
};
|
|
603
|
+
this.applyColors(currentColors);
|
|
604
|
+
this.saveToStorage();
|
|
605
|
+
}
|
|
606
|
+
/** 切换暗色模式 */
|
|
607
|
+
toggleDarkMode() {
|
|
608
|
+
this.setDarkMode(!this.isDark);
|
|
609
|
+
return this.isDark;
|
|
610
|
+
}
|
|
611
|
+
/** 启用系统主题跟随 */
|
|
612
|
+
enableSystemFollow() {
|
|
613
|
+
if (typeof window === "undefined") return;
|
|
614
|
+
this.followSystem = true;
|
|
615
|
+
this.systemDarkQuery = window.matchMedia("(prefers-color-scheme: dark)");
|
|
616
|
+
this.setDarkMode(this.systemDarkQuery.matches);
|
|
617
|
+
this.systemDarkHandler = (e) => {
|
|
618
|
+
if (this.followSystem) {
|
|
619
|
+
this.setDarkMode(e.matches);
|
|
620
|
+
}
|
|
621
|
+
};
|
|
622
|
+
this.systemDarkQuery.addEventListener("change", this.systemDarkHandler);
|
|
623
|
+
}
|
|
624
|
+
/** 禁用系统主题跟随 */
|
|
625
|
+
disableSystemFollow() {
|
|
626
|
+
this.followSystem = false;
|
|
627
|
+
if (this.systemDarkQuery && this.systemDarkHandler) {
|
|
628
|
+
this.systemDarkQuery.removeEventListener("change", this.systemDarkHandler);
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
/** 设置响应式主题 */
|
|
632
|
+
setResponsiveTheme(config) {
|
|
633
|
+
this.responsiveConfig = config;
|
|
634
|
+
if (typeof window === "undefined") return;
|
|
635
|
+
this.currentBreakpoint = getCurrentBreakpoint();
|
|
636
|
+
this.state.breakpoint = this.currentBreakpoint;
|
|
637
|
+
this.applyResponsiveTheme();
|
|
638
|
+
this.resizeHandler = () => {
|
|
639
|
+
const newBreakpoint = getCurrentBreakpoint();
|
|
640
|
+
if (newBreakpoint !== this.currentBreakpoint) {
|
|
641
|
+
this.currentBreakpoint = newBreakpoint;
|
|
642
|
+
this.state.breakpoint = newBreakpoint;
|
|
643
|
+
this.applyResponsiveTheme();
|
|
644
|
+
}
|
|
645
|
+
};
|
|
646
|
+
window.addEventListener("resize", this.resizeHandler);
|
|
647
|
+
}
|
|
648
|
+
/** 应用响应式主题 */
|
|
649
|
+
applyResponsiveTheme() {
|
|
650
|
+
const config = this.responsiveConfig[this.currentBreakpoint];
|
|
651
|
+
if (config) {
|
|
652
|
+
this.apply(config);
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
/** 获取当前是否暗色模式 */
|
|
656
|
+
get dark() {
|
|
657
|
+
return this.isDark;
|
|
658
|
+
}
|
|
659
|
+
/** 获取当前主题 */
|
|
660
|
+
get theme() {
|
|
661
|
+
return this.currentTheme;
|
|
662
|
+
}
|
|
663
|
+
/** 获取所有可用预设 */
|
|
664
|
+
get presets() {
|
|
665
|
+
return Object.keys(presetThemes);
|
|
666
|
+
}
|
|
667
|
+
/** 获取当前断点 */
|
|
668
|
+
get breakpoint() {
|
|
669
|
+
return this.currentBreakpoint;
|
|
670
|
+
}
|
|
671
|
+
/** 应用颜色到 CSS 变量 */
|
|
672
|
+
applyColors(colors) {
|
|
673
|
+
const el = this.targetEl || (typeof document !== "undefined" ? document.documentElement : null);
|
|
674
|
+
if (!el) return;
|
|
675
|
+
const styles = this.getThemeStyles(colors);
|
|
676
|
+
Object.entries(styles).forEach(([name, value]) => {
|
|
677
|
+
setCssVar(name, value, el);
|
|
678
|
+
});
|
|
679
|
+
}
|
|
680
|
+
/** 获取当前主题的 CSS 变量对象 */
|
|
681
|
+
getThemeStyles(colors = {}) {
|
|
682
|
+
const styles = {};
|
|
683
|
+
const themeColors = {
|
|
684
|
+
...presetThemes[this.currentTheme],
|
|
685
|
+
...this.customColors,
|
|
686
|
+
...colors
|
|
687
|
+
};
|
|
688
|
+
const colorTypes = [
|
|
689
|
+
{ key: "primary", cssVar: "primary" },
|
|
690
|
+
{ key: "success", cssVar: "success" },
|
|
691
|
+
{ key: "warning", cssVar: "warning" },
|
|
692
|
+
{ key: "danger", cssVar: "danger" },
|
|
693
|
+
{ key: "info", cssVar: "info" }
|
|
694
|
+
];
|
|
695
|
+
colorTypes.forEach(({ key, cssVar }) => {
|
|
696
|
+
const baseColor = themeColors[key];
|
|
697
|
+
if (baseColor) {
|
|
698
|
+
const colorScale = generateColorScaleWithAlgorithm(baseColor, this.isDark, this.algorithm);
|
|
699
|
+
Object.entries(colorScale).forEach(([suffix, value]) => {
|
|
700
|
+
const varName = suffix ? `--yh-color-${cssVar}-${suffix}` : `--yh-color-${cssVar}`;
|
|
701
|
+
styles[varName] = value;
|
|
702
|
+
});
|
|
703
|
+
const semanticColors = generateSemanticColors(baseColor, this.isDark);
|
|
704
|
+
Object.entries(semanticColors).forEach(([state, value]) => {
|
|
705
|
+
styles[`--yh-color-${cssVar}-${state}`] = value;
|
|
706
|
+
});
|
|
707
|
+
const rgb = hexToRgb(baseColor);
|
|
708
|
+
if (rgb) {
|
|
709
|
+
styles[`--yh-color-${cssVar}-rgb`] = `${rgb.r}, ${rgb.g}, ${rgb.b}`;
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
});
|
|
713
|
+
return styles;
|
|
714
|
+
}
|
|
715
|
+
/** 获取 CSS 变量值 */
|
|
716
|
+
getCssVar(name) {
|
|
717
|
+
return getCssVar(name, this.targetEl);
|
|
718
|
+
}
|
|
719
|
+
/** 设置 CSS 变量值 */
|
|
720
|
+
setCssVar(name, value) {
|
|
721
|
+
setCssVar(name, value, this.targetEl);
|
|
722
|
+
}
|
|
723
|
+
/** 应用所有设计令牌 */
|
|
724
|
+
applyTokens() {
|
|
725
|
+
const el = this.targetEl || (typeof document !== "undefined" ? document.documentElement : null);
|
|
726
|
+
if (!el) return;
|
|
727
|
+
Object.entries(designTokens.colors).forEach(([type, colors]) => {
|
|
728
|
+
const colorObj = colors;
|
|
729
|
+
setCssVar(`--yh-color-${type}`, colorObj.DEFAULT, el);
|
|
730
|
+
if (colorObj.light) {
|
|
731
|
+
Object.entries(colorObj.light).forEach(([level, value]) => {
|
|
732
|
+
setCssVar(`--yh-color-${type}-light-${level}`, value, el);
|
|
733
|
+
});
|
|
734
|
+
}
|
|
735
|
+
if (colorObj.dark) {
|
|
736
|
+
Object.entries(colorObj.dark).forEach(([level, value]) => {
|
|
737
|
+
setCssVar(`--yh-color-${type}-dark-${level}`, value, el);
|
|
738
|
+
});
|
|
739
|
+
}
|
|
740
|
+
});
|
|
741
|
+
Object.entries(designTokens.textColors).forEach(([key, value]) => {
|
|
742
|
+
setCssVar(`--yh-text-color-${key}`, value, el);
|
|
743
|
+
});
|
|
744
|
+
Object.entries(designTokens.borderColors).forEach(([key, value]) => {
|
|
745
|
+
const name = key === "DEFAULT" ? "--yh-border-color" : `--yh-border-color-${key}`;
|
|
746
|
+
setCssVar(name, value, el);
|
|
747
|
+
});
|
|
748
|
+
Object.entries(designTokens.bgColors).forEach(([key, value]) => {
|
|
749
|
+
const name = key === "DEFAULT" ? "--yh-bg-color" : `--yh-bg-color-${key}`;
|
|
750
|
+
setCssVar(name, value, el);
|
|
751
|
+
});
|
|
752
|
+
Object.entries(designTokens.radius).forEach(([key, value]) => {
|
|
753
|
+
setCssVar(`--yh-radius-${key}`, value, el);
|
|
754
|
+
});
|
|
755
|
+
Object.entries(designTokens.zIndex).forEach(([key, value]) => {
|
|
756
|
+
setCssVar(`--yh-z-index-${key}`, value, el);
|
|
757
|
+
});
|
|
758
|
+
}
|
|
759
|
+
// ==================== 持久化 ====================
|
|
760
|
+
/** 保存到存储 */
|
|
761
|
+
saveToStorage() {
|
|
762
|
+
if (typeof localStorage === "undefined") return;
|
|
763
|
+
const snapshot = {
|
|
764
|
+
preset: this.currentTheme,
|
|
765
|
+
colors: this.customColors,
|
|
766
|
+
dark: this.isDark,
|
|
767
|
+
radius: "md",
|
|
768
|
+
algorithm: this.algorithm,
|
|
769
|
+
density: this.currentDensity,
|
|
770
|
+
timestamp: Date.now(),
|
|
771
|
+
version: "1.0.0"
|
|
772
|
+
};
|
|
773
|
+
try {
|
|
774
|
+
localStorage.setItem(this.persistKey, JSON.stringify(snapshot));
|
|
775
|
+
} catch (e) {
|
|
776
|
+
console.warn("[YH-UI Theme] Failed to persist theme:", e);
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
/** 从存储恢复 */
|
|
780
|
+
restoreFromStorage() {
|
|
781
|
+
if (typeof localStorage === "undefined") return false;
|
|
782
|
+
try {
|
|
783
|
+
const stored = localStorage.getItem(this.persistKey);
|
|
784
|
+
if (!stored) return false;
|
|
785
|
+
const snapshot = JSON.parse(stored);
|
|
786
|
+
this.currentTheme = snapshot.preset;
|
|
787
|
+
this.customColors = snapshot.colors || {};
|
|
788
|
+
this.isDark = snapshot.dark;
|
|
789
|
+
this.algorithm = snapshot.algorithm || "default";
|
|
790
|
+
this.currentDensity = snapshot.density || "comfortable";
|
|
791
|
+
this.state.theme = this.currentTheme;
|
|
792
|
+
this.state.dark = this.isDark;
|
|
793
|
+
this.state.colors = this.customColors;
|
|
794
|
+
this.state.density = this.currentDensity;
|
|
795
|
+
return true;
|
|
796
|
+
} catch (e) {
|
|
797
|
+
console.warn("[YH-UI Theme] Failed to restore theme:", e);
|
|
798
|
+
return false;
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
/** 导出主题配置 */
|
|
802
|
+
exportTheme(options) {
|
|
803
|
+
const snapshot = {
|
|
804
|
+
preset: this.currentTheme,
|
|
805
|
+
colors: this.customColors,
|
|
806
|
+
dark: this.isDark,
|
|
807
|
+
radius: "md",
|
|
808
|
+
algorithm: this.algorithm,
|
|
809
|
+
density: this.currentDensity,
|
|
810
|
+
timestamp: Date.now(),
|
|
811
|
+
version: "1.0.0",
|
|
812
|
+
name: options?.name,
|
|
813
|
+
author: options?.author
|
|
814
|
+
};
|
|
815
|
+
return JSON.stringify(snapshot, null, 2);
|
|
816
|
+
}
|
|
817
|
+
/** 导入主题配置 */
|
|
818
|
+
importTheme(json) {
|
|
819
|
+
try {
|
|
820
|
+
const snapshot = JSON.parse(json);
|
|
821
|
+
if (this.transitionEnabled) {
|
|
822
|
+
enableThemeTransition(this.transitionConfig.duration, this.transitionConfig.timing);
|
|
823
|
+
}
|
|
824
|
+
this.setPreset(snapshot.preset);
|
|
825
|
+
if (snapshot.colors && Object.keys(snapshot.colors).length > 0) {
|
|
826
|
+
this.setColors(snapshot.colors);
|
|
827
|
+
}
|
|
828
|
+
this.setDarkMode(snapshot.dark);
|
|
829
|
+
if (snapshot.algorithm) {
|
|
830
|
+
this.setAlgorithm(snapshot.algorithm);
|
|
831
|
+
}
|
|
832
|
+
if (snapshot.density) {
|
|
833
|
+
this.setDensity(snapshot.density);
|
|
834
|
+
}
|
|
835
|
+
return true;
|
|
836
|
+
} catch (e) {
|
|
837
|
+
console.error("[YH-UI Theme] Failed to import theme:", e);
|
|
838
|
+
return false;
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
/** 导出为纯 CSS */
|
|
842
|
+
exportAsCss() {
|
|
843
|
+
const styles = this.getThemeStyles();
|
|
844
|
+
let css = ":root {\n";
|
|
845
|
+
Object.entries(styles).forEach(([name, value]) => {
|
|
846
|
+
css += ` ${name}: ${value};
|
|
847
|
+
`;
|
|
848
|
+
});
|
|
849
|
+
css += "}\n";
|
|
850
|
+
return css;
|
|
851
|
+
}
|
|
852
|
+
/** 重置为默认主题 */
|
|
853
|
+
reset() {
|
|
854
|
+
if (typeof document === "undefined") return;
|
|
855
|
+
this.currentTheme = "default";
|
|
856
|
+
this.customColors = {};
|
|
857
|
+
this.isDark = false;
|
|
858
|
+
this.algorithm = "default";
|
|
859
|
+
this.currentDensity = "comfortable";
|
|
860
|
+
this.colorBlindMode = "none";
|
|
861
|
+
this.componentOverrides = {};
|
|
862
|
+
this.state.theme = "default";
|
|
863
|
+
this.state.dark = false;
|
|
864
|
+
this.state.colors = {};
|
|
865
|
+
this.state.density = "comfortable";
|
|
866
|
+
this.state.colorBlindMode = "none";
|
|
867
|
+
document.documentElement.classList.remove("dark");
|
|
868
|
+
const el = this.targetEl || document.documentElement;
|
|
869
|
+
const colorTypes = ["primary", "success", "warning", "danger", "info"];
|
|
870
|
+
const suffixes = [
|
|
871
|
+
"",
|
|
872
|
+
"light-1",
|
|
873
|
+
"light-2",
|
|
874
|
+
"light-3",
|
|
875
|
+
"light-4",
|
|
876
|
+
"light-5",
|
|
877
|
+
"light-6",
|
|
878
|
+
"light-7",
|
|
879
|
+
"light-8",
|
|
880
|
+
"light-9",
|
|
881
|
+
"dark-2",
|
|
882
|
+
"hover",
|
|
883
|
+
"active",
|
|
884
|
+
"disabled",
|
|
885
|
+
"focus",
|
|
886
|
+
"rgb"
|
|
887
|
+
];
|
|
888
|
+
colorTypes.forEach((type) => {
|
|
889
|
+
suffixes.forEach((suffix) => {
|
|
890
|
+
const varName = suffix ? `--yh-color-${type}-${suffix}` : `--yh-color-${type}`;
|
|
891
|
+
removeCssVar(varName, el);
|
|
892
|
+
});
|
|
893
|
+
});
|
|
894
|
+
Object.keys(densityConfig.comfortable).forEach((key) => {
|
|
895
|
+
removeCssVar(key, el);
|
|
896
|
+
});
|
|
897
|
+
this.saveToStorage();
|
|
898
|
+
}
|
|
899
|
+
// ==================== 密度/紧凑度控制 ====================
|
|
900
|
+
/** 设置密度 */
|
|
901
|
+
setDensity(density) {
|
|
902
|
+
if (this.transitionEnabled) {
|
|
903
|
+
enableThemeTransition(this.transitionConfig.duration, this.transitionConfig.timing);
|
|
904
|
+
}
|
|
905
|
+
this.currentDensity = density;
|
|
906
|
+
this.state.density = density;
|
|
907
|
+
const el = this.targetEl || (typeof document !== "undefined" ? document.documentElement : null);
|
|
908
|
+
if (!el) return;
|
|
909
|
+
const config = densityConfig[density];
|
|
910
|
+
Object.entries(config).forEach(([name, value]) => {
|
|
911
|
+
setCssVar(name, value, el);
|
|
912
|
+
});
|
|
913
|
+
this.saveToStorage();
|
|
914
|
+
}
|
|
915
|
+
/** 获取当前密度 */
|
|
916
|
+
get density() {
|
|
917
|
+
return this.currentDensity;
|
|
918
|
+
}
|
|
919
|
+
// ==================== 色盲模式 ====================
|
|
920
|
+
/** 设置色盲友好模式 */
|
|
921
|
+
setColorBlindMode(mode) {
|
|
922
|
+
if (this.transitionEnabled) {
|
|
923
|
+
enableThemeTransition(this.transitionConfig.duration, this.transitionConfig.timing);
|
|
924
|
+
}
|
|
925
|
+
this.colorBlindMode = mode;
|
|
926
|
+
this.state.colorBlindMode = mode;
|
|
927
|
+
if (mode === "none") {
|
|
928
|
+
const currentColors = {
|
|
929
|
+
...presetThemes[this.currentTheme],
|
|
930
|
+
...this.customColors
|
|
931
|
+
};
|
|
932
|
+
this.applyColors(currentColors);
|
|
933
|
+
} else {
|
|
934
|
+
const palette = colorBlindPalettes[mode];
|
|
935
|
+
this.applyColors(palette);
|
|
936
|
+
}
|
|
937
|
+
this.saveToStorage();
|
|
938
|
+
}
|
|
939
|
+
/** 获取当前色盲模式 */
|
|
940
|
+
get colorBlind() {
|
|
941
|
+
return this.colorBlindMode;
|
|
942
|
+
}
|
|
943
|
+
// ==================== 组件级主题覆盖 ====================
|
|
944
|
+
/** 设置组件级主题覆盖 */
|
|
945
|
+
setComponentTheme(componentName, overrides) {
|
|
946
|
+
this.componentOverrides[componentName] = {
|
|
947
|
+
...this.componentOverrides[componentName],
|
|
948
|
+
...overrides
|
|
949
|
+
};
|
|
950
|
+
const el = this.targetEl || (typeof document !== "undefined" ? document.documentElement : null);
|
|
951
|
+
if (!el) return;
|
|
952
|
+
Object.entries(overrides).forEach(([name, value]) => {
|
|
953
|
+
setCssVar(`--yh-${componentName}-${name}`, value, el);
|
|
954
|
+
});
|
|
955
|
+
}
|
|
956
|
+
/** 获取组件主题覆盖 */
|
|
957
|
+
getComponentTheme(componentName) {
|
|
958
|
+
return this.componentOverrides[componentName] || {};
|
|
959
|
+
}
|
|
960
|
+
/** 清除组件级覆盖 */
|
|
961
|
+
clearComponentTheme(componentName) {
|
|
962
|
+
const overrides = this.componentOverrides[componentName];
|
|
963
|
+
if (!overrides) return;
|
|
964
|
+
const el = this.targetEl || (typeof document !== "undefined" ? document.documentElement : null);
|
|
965
|
+
if (!el) return;
|
|
966
|
+
Object.keys(overrides).forEach((name) => {
|
|
967
|
+
removeCssVar(`--yh-${componentName}-${name}`, el);
|
|
968
|
+
});
|
|
969
|
+
delete this.componentOverrides[componentName];
|
|
970
|
+
}
|
|
971
|
+
// ==================== 主题切换动画 ====================
|
|
972
|
+
/** 启用主题切换动画 */
|
|
973
|
+
enableTransition(config) {
|
|
974
|
+
this.transitionEnabled = true;
|
|
975
|
+
if (config) {
|
|
976
|
+
this.transitionConfig = {
|
|
977
|
+
duration: config.duration ?? DEFAULT_TRANSITION_DURATION,
|
|
978
|
+
timing: config.timing ?? DEFAULT_TRANSITION_TIMING
|
|
979
|
+
};
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
/** 禁用主题切换动画 */
|
|
983
|
+
disableTransition() {
|
|
984
|
+
this.transitionEnabled = false;
|
|
985
|
+
}
|
|
986
|
+
// ==================== 主题继承与合并 ====================
|
|
987
|
+
/** 创建继承主题 */
|
|
988
|
+
createTheme(config) {
|
|
989
|
+
let baseColors = {};
|
|
990
|
+
if (config.extends) {
|
|
991
|
+
if (typeof config.extends === "string") {
|
|
992
|
+
baseColors = { ...presetThemes[config.extends] };
|
|
993
|
+
} else {
|
|
994
|
+
const parentSnapshot = this.createTheme(config.extends);
|
|
995
|
+
baseColors = { ...parentSnapshot.colors };
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
const mergedColors = { ...baseColors, ...config.colors };
|
|
999
|
+
return {
|
|
1000
|
+
preset: config.preset || "default",
|
|
1001
|
+
colors: mergedColors,
|
|
1002
|
+
dark: config.dark || false,
|
|
1003
|
+
radius: config.radius || "md",
|
|
1004
|
+
algorithm: config.algorithm || "default",
|
|
1005
|
+
density: config.density || "comfortable",
|
|
1006
|
+
timestamp: Date.now(),
|
|
1007
|
+
version: "1.0.0",
|
|
1008
|
+
name: config.name,
|
|
1009
|
+
author: config.author
|
|
1010
|
+
};
|
|
1011
|
+
}
|
|
1012
|
+
/** 应用完整主题配置 */
|
|
1013
|
+
applyFullConfig(config) {
|
|
1014
|
+
if (this.transitionEnabled || config.transition) {
|
|
1015
|
+
const transitionConfig = typeof config.transition === "object" ? config.transition : { duration: DEFAULT_TRANSITION_DURATION, timing: DEFAULT_TRANSITION_TIMING };
|
|
1016
|
+
enableThemeTransition(transitionConfig.duration, transitionConfig.timing);
|
|
1017
|
+
}
|
|
1018
|
+
this.pushHistory();
|
|
1019
|
+
const snapshot = this.createTheme(config);
|
|
1020
|
+
this.setPreset(snapshot.preset);
|
|
1021
|
+
if (snapshot.colors && Object.keys(snapshot.colors).length > 0) {
|
|
1022
|
+
this.setColors(snapshot.colors);
|
|
1023
|
+
}
|
|
1024
|
+
this.setDarkMode(snapshot.dark);
|
|
1025
|
+
if (snapshot.algorithm) {
|
|
1026
|
+
this.setAlgorithm(snapshot.algorithm);
|
|
1027
|
+
}
|
|
1028
|
+
if (config.density) {
|
|
1029
|
+
this.setDensity(config.density);
|
|
1030
|
+
}
|
|
1031
|
+
if (config.colorBlindMode) {
|
|
1032
|
+
this.setColorBlindMode(config.colorBlindMode);
|
|
1033
|
+
}
|
|
1034
|
+
if (config.components) {
|
|
1035
|
+
Object.entries(config.components).forEach(([componentName, overrides]) => {
|
|
1036
|
+
this.setComponentTheme(componentName, overrides);
|
|
1037
|
+
});
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
// ==================== 主题历史 ====================
|
|
1041
|
+
/** 保存当前状态到历史 */
|
|
1042
|
+
pushHistory() {
|
|
1043
|
+
const snapshot = {
|
|
1044
|
+
preset: this.currentTheme,
|
|
1045
|
+
colors: { ...this.customColors },
|
|
1046
|
+
dark: this.isDark,
|
|
1047
|
+
radius: "md",
|
|
1048
|
+
algorithm: this.algorithm,
|
|
1049
|
+
density: this.currentDensity,
|
|
1050
|
+
timestamp: Date.now(),
|
|
1051
|
+
version: "1.0.0"
|
|
1052
|
+
};
|
|
1053
|
+
this.themeHistory.push(snapshot);
|
|
1054
|
+
if (this.themeHistory.length > this.maxHistoryLength) {
|
|
1055
|
+
this.themeHistory.shift();
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
/** 撤销到上一个主题状态 */
|
|
1059
|
+
undo() {
|
|
1060
|
+
const previousSnapshot = this.themeHistory.pop();
|
|
1061
|
+
if (!previousSnapshot) return false;
|
|
1062
|
+
this.importTheme(JSON.stringify(previousSnapshot));
|
|
1063
|
+
return true;
|
|
1064
|
+
}
|
|
1065
|
+
/** 获取主题历史 */
|
|
1066
|
+
getHistory() {
|
|
1067
|
+
return [...this.themeHistory];
|
|
1068
|
+
}
|
|
1069
|
+
/** 清除主题历史 */
|
|
1070
|
+
clearHistory() {
|
|
1071
|
+
this.themeHistory = [];
|
|
1072
|
+
}
|
|
1073
|
+
// ==================== 智能色彩生成 ====================
|
|
1074
|
+
/** 从主色生成完整调色板 */
|
|
1075
|
+
generateFromPrimary(primaryColor) {
|
|
1076
|
+
return generatePaletteFromPrimary(primaryColor);
|
|
1077
|
+
}
|
|
1078
|
+
/** 应用从主色生成的调色板 */
|
|
1079
|
+
applyFromPrimary(primaryColor) {
|
|
1080
|
+
const palette = this.generateFromPrimary(primaryColor);
|
|
1081
|
+
this.setColors(palette);
|
|
1082
|
+
}
|
|
1083
|
+
/** 获取互补色 */
|
|
1084
|
+
getComplementary(hex) {
|
|
1085
|
+
return getComplementaryColor(hex);
|
|
1086
|
+
}
|
|
1087
|
+
/** 获取类似色 */
|
|
1088
|
+
getAnalogous(hex) {
|
|
1089
|
+
return getAnalogousColors(hex);
|
|
1090
|
+
}
|
|
1091
|
+
/** 获取三角色 */
|
|
1092
|
+
getTriadic(hex) {
|
|
1093
|
+
return getTriadicColors(hex);
|
|
1094
|
+
}
|
|
1095
|
+
// ==================== 响应式主题变量 ====================
|
|
1096
|
+
/** 设置响应式变量 (根据断点自动切换) */
|
|
1097
|
+
setResponsiveVar(name, values) {
|
|
1098
|
+
if (typeof document === "undefined") return;
|
|
1099
|
+
let style = document.getElementById("yh-responsive-vars");
|
|
1100
|
+
if (!style) {
|
|
1101
|
+
style = document.createElement("style");
|
|
1102
|
+
style.id = "yh-responsive-vars";
|
|
1103
|
+
document.head.appendChild(style);
|
|
1104
|
+
}
|
|
1105
|
+
let css = "";
|
|
1106
|
+
const orderedBreakpoints = ["xs", "sm", "md", "lg", "xl", "xxl"];
|
|
1107
|
+
orderedBreakpoints.forEach((bp) => {
|
|
1108
|
+
if (values[bp]) {
|
|
1109
|
+
if (bp === "xs") {
|
|
1110
|
+
css += `
|
|
1111
|
+
:root { ${name}: ${values[bp]}; }
|
|
1112
|
+
`;
|
|
1113
|
+
} else {
|
|
1114
|
+
css += `
|
|
1115
|
+
@media (min-width: ${breakpoints[bp]}px) {
|
|
1116
|
+
:root { ${name}: ${values[bp]}; }
|
|
1117
|
+
}
|
|
1118
|
+
`;
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
});
|
|
1122
|
+
style.textContent += css;
|
|
1123
|
+
}
|
|
1124
|
+
/** 销毁 */
|
|
1125
|
+
destroy() {
|
|
1126
|
+
if (this.resizeHandler && typeof window !== "undefined") {
|
|
1127
|
+
window.removeEventListener("resize", this.resizeHandler);
|
|
1128
|
+
}
|
|
1129
|
+
this.disableSystemFollow();
|
|
1130
|
+
const style = document.getElementById("yh-responsive-vars");
|
|
1131
|
+
if (style) {
|
|
1132
|
+
style.remove();
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
let globalThemeManager = null;
|
|
1137
|
+
export function useTheme() {
|
|
1138
|
+
if (!globalThemeManager) {
|
|
1139
|
+
globalThemeManager = new ThemeManager();
|
|
1140
|
+
}
|
|
1141
|
+
return globalThemeManager;
|
|
1142
|
+
}
|
|
1143
|
+
export function setThemeColor(color) {
|
|
1144
|
+
useTheme().setPrimaryColor(color);
|
|
1145
|
+
}
|
|
1146
|
+
export function toggleDarkMode() {
|
|
1147
|
+
return useTheme().toggleDarkMode();
|
|
1148
|
+
}
|
|
1149
|
+
export function setThemePreset(preset) {
|
|
1150
|
+
useTheme().setPreset(preset);
|
|
1151
|
+
}
|
|
1152
|
+
export function initTheme(options) {
|
|
1153
|
+
globalThemeManager = new ThemeManager(options);
|
|
1154
|
+
return globalThemeManager;
|
|
1155
|
+
}
|
|
1156
|
+
export function useThemeVars() {
|
|
1157
|
+
const theme = useTheme();
|
|
1158
|
+
return {
|
|
1159
|
+
...toRefs(theme.state),
|
|
1160
|
+
getCssVar: (name) => theme.getCssVar(name),
|
|
1161
|
+
setCssVar: (name, value) => theme.setCssVar(name, value)
|
|
1162
|
+
};
|
|
1163
|
+
}
|
|
1164
|
+
export function checkContrast(foreground, background, level = "AA") {
|
|
1165
|
+
const ratio = getContrastRatio(foreground, background);
|
|
1166
|
+
return level === "AAA" ? ratio >= 7 : ratio >= 4.5;
|
|
1167
|
+
}
|
|
1168
|
+
export function getTextColorForBackground(background) {
|
|
1169
|
+
const rgb = hexToRgb(background);
|
|
1170
|
+
if (!rgb) return "#000000";
|
|
1171
|
+
const luminance = getRelativeLuminance(rgb.r, rgb.g, rgb.b);
|
|
1172
|
+
return luminance > 0.5 ? "#000000" : "#ffffff";
|
|
1173
|
+
}
|
|
1174
|
+
export const THEME_INJECTION_KEY = Symbol("theme");
|
|
1175
|
+
export const ThemePlugin = {
|
|
1176
|
+
install(app, options) {
|
|
1177
|
+
const themeManager = initTheme(options);
|
|
1178
|
+
app.config.globalProperties.$theme = themeManager;
|
|
1179
|
+
app.provide(THEME_INJECTION_KEY, themeManager);
|
|
1180
|
+
app.provide("theme", themeManager);
|
|
1181
|
+
}
|
|
1182
|
+
};
|
|
1183
|
+
export default ThemePlugin;
|
|
1184
|
+
export {
|
|
1185
|
+
hexToRgb,
|
|
1186
|
+
rgbToHex,
|
|
1187
|
+
rgbToHsl,
|
|
1188
|
+
hslToRgb,
|
|
1189
|
+
mixColor,
|
|
1190
|
+
adjustSaturation,
|
|
1191
|
+
adjustLightness,
|
|
1192
|
+
getContrastRatio,
|
|
1193
|
+
ensureContrast,
|
|
1194
|
+
getRelativeLuminance,
|
|
1195
|
+
getComplementaryColor,
|
|
1196
|
+
getAnalogousColors,
|
|
1197
|
+
getTriadicColors,
|
|
1198
|
+
generatePaletteFromPrimary
|
|
1199
|
+
};
|
|
1200
|
+
export { presetThemes, breakpoints, densityConfig, colorBlindPalettes };
|