af68b20f87
新增 src/styles/theme.ts 语义 token 单源 + 相对亮度/对比度/isDarkSurface, tests 校验侧边栏暗、内容区亮、正文 WCAG AA、与 main.css 变量交叉一致防漂移。
70 lines
2.4 KiB
TypeScript
70 lines
2.4 KiB
TypeScript
/**
|
||
* Admin 主题 token 单源(任务 4):深色侧边栏 + 浅色内容区。
|
||
* main.css 使用同名 --admin-* CSS 变量承载渲染值;tests 用本模块校验
|
||
* “侧边栏为暗、内容区为亮、正文对比度达标”的不变量,防止误改成浅色侧栏。
|
||
*/
|
||
export const ADMIN_THEME = {
|
||
sidebar: '#192132',
|
||
sidebarDeep: '#121827',
|
||
primary: '#6366f1',
|
||
primarySoft: '#eef0ff',
|
||
bg: '#f3f5f9',
|
||
border: '#e5e7ef',
|
||
text: '#1f2937',
|
||
muted: '#7b8495',
|
||
} as const
|
||
|
||
export type AdminThemeKey = keyof typeof ADMIN_THEME
|
||
|
||
export const ADMIN_THEME_KEYS = Object.keys(ADMIN_THEME) as AdminThemeKey[]
|
||
|
||
/** CSS 变量名:camelCase -> kebab(sidebarDeep -> --admin-sidebar-deep)。 */
|
||
export function cssVarName(key: AdminThemeKey): string {
|
||
const kebab = key.replace(/[A-Z]/g, (c) => `-${c.toLowerCase()}`)
|
||
return `--admin-${kebab}`
|
||
}
|
||
|
||
function parseHex(hex: string): { r: number; g: number; b: number } {
|
||
if (!/^#[0-9a-fA-F]{6}$/.test(hex)) {
|
||
throw new Error(`主题色必须是 #rrggbb 六位十六进制: ${hex}`)
|
||
}
|
||
const raw = hex.slice(1)
|
||
return {
|
||
r: parseInt(raw.slice(0, 2), 16),
|
||
g: parseInt(raw.slice(2, 4), 16),
|
||
b: parseInt(raw.slice(4, 6), 16),
|
||
}
|
||
}
|
||
|
||
/** sRGB 通道 -> 线性亮度。 */
|
||
function channelLuminance(channel: number): number {
|
||
const s = channel / 255
|
||
return s <= 0.03928 ? s / 12.92 : ((s + 0.055) / 1.055) ** 2.4
|
||
}
|
||
|
||
/** 相对亮度(0 纯黑 ~ 1 纯白)。 */
|
||
export function relativeLuminance(hex: string): number {
|
||
const { r, g, b } = parseHex(hex)
|
||
return 0.2126 * channelLuminance(r) + 0.7152 * channelLuminance(g) + 0.0722 * channelLuminance(b)
|
||
}
|
||
|
||
export function isDarkSurface(hex: string, threshold = 0.2): boolean {
|
||
return relativeLuminance(hex) < threshold
|
||
}
|
||
|
||
/** WCAG 对比度(1 ~ 21),fg/bg 可为 8 位 hex。 */
|
||
export function contrastRatio(fgHex: string, bgHex: string): number {
|
||
const lighter = Math.max(relativeLuminance(fgHex), relativeLuminance(bgHex))
|
||
const darker = Math.min(relativeLuminance(fgHex), relativeLuminance(bgHex))
|
||
return (lighter + 0.05) / (darker + 0.05)
|
||
}
|
||
|
||
/** 取某个语义 token 的 hex;未知 key 给出可操作错误。 */
|
||
export function tokenHex(key: AdminThemeKey): string {
|
||
const value = ADMIN_THEME[key]
|
||
if (!value) {
|
||
throw new Error(`未知 Admin 主题 token: ${String(key)},允许值: ${ADMIN_THEME_KEYS.join(', ')}`)
|
||
}
|
||
return value
|
||
}
|