Skip to content

主題系統總覽

This content is not available in your language yet.

lh-ui 的主題系統是雙源的,這一點必須先講清楚:

源 內容 消費方式
styles/theme/common/var.scss + var-dark.scss CSS 自訂屬性(--lh-*) 元件 SCSS 裡 var(--lh-*),執行時求值
styles/theme/common/variables.scss SCSS 變數($lh-*) 編譯期烤死成具體值,用於計算(顏色函式、陰影合成等)
src/theme/themes/index.ts 5 個預設的 vars 映射 JS 執行時注入 <style>(ConfigProvider / ThemeStudio)

元件只認 CSS 變數。JS 側的主題系統存在的意義是:把一套設計令牌在執行時注入到 DOM 的某個子樹上,讓元件無需重新編譯就換膚。

五個內建預設:青花瓷 / 松花綠 / 朱砂 / 水墨 / 藤黃。UI 層用 buildPresetStyleText(vars) 生成 <style> 文字寫入 DOM。

import { LH_THEME_PRESETS, buildPresetStyleText, LH_PRESET_THEME_STYLE_ID } from '@lh-ui/share/theme'
const preset = LH_THEME_PRESETS[0] // 青花瓷
const styleText = buildPresetStyleText(preset.vars) // 拼成 CSS 文字
// → 注入到 <style id="lh-preset-theme">

詳見 主題預設。

ConfigProvider 接收 themeVars,鍵為簡寫(自動補 --lh- 前綴):

<lh-config-provider :theme-vars="{ 'primary-color': '#1677ff' }">
<App />
</lh-config-provider>

詳見 執行時客製。

src/theme/index.ts 匯出(實作在 src/theme/themes/index.ts):

原語 簽名 用途
LH_THEME_PRESETS LhThemePreset[] 全部預設陣列,docs 與 ConfigProvider 遍歷它
registerTheme(preset) (preset: LhThemePreset) => void 註冊或覆蓋預設(同名覆蓋,內建預設也可覆蓋)
getThemePreset(name) (name: LhThemePresetName) => LhThemePreset | undefined 按名取預設
getThemeVars(name) (name: LhThemePresetName) => LhThemeVars | undefined 按名取其 vars(未註冊傳回 undefined)
buildPresetStyleText(vars) (vars: Record<string, string>) => string 把 vars 拼成可注入的 <style> 文字

另有對比度計算原語(verify:theme 與 ThemeStudio 用):

原語 簽名 用途
getRelativeLuminance(color) (color: string | undefined) => number | undefined WCAG 相對亮度(非 hex 傳回 undefined)
getContrastRatio(a, b) (a, b: string | undefined) => number | undefined WCAG 對比度(1~21)
buildPresetDarkVars(vars) (vars: Record<string, string>) => Record<string, string> 由預設 vars 推導暗色塊變數
LH_PRESET_THEME_STYLE_ID 'lh-preset-theme' 注入元素的固定 id
export type LhThemePresetName =
| 'cerblue' | 'songhua' | 'zhusha' | 'shuimo' | 'golden'
| (string & {}) // ← 開放自訂名稱,同時保留字面量自動補全
export interface LhThemePreset {
name: LhThemePresetName
label: string // 中文名稱
labelEn: string // 英文名稱
description: string // 主題描述
vars: Record<string, string> // CSS 變數映射
}
export type LhThemeVars = Record<string, string>

LhThemePresetName 結尾的 (string & {}) 是 TS 技巧:保持前五個字面量的自動補全,同時允許 registerTheme({ name: 'my-brand', ... }) 傳任意字串。未註冊的名字在 getThemePreset / getThemeVars 中傳回 undefined。

注入約束:只能寫 <style>,不能寫內聯 style

Section titled “注入約束:只能寫 <style>,不能寫內聯 style”

這是主題系統最重要的約束,踩過一次坑:

// ✅ 正確:寫 <style> 元素,特異性 0,1,0
const el = document.createElement('style')
el.id = LH_PRESET_THEME_STYLE_ID
el.textContent = buildPresetStyleText(vars)
document.head.appendChild(el)
// ❌ 錯誤:寫 documentElement 的內聯 style,特異性 1,0,0,0
Object.assign(document.documentElement.style, vars)

為什麼:內聯 style 的特異性高於任何樣式表,會壓過 var-dark.scss 裡整段的暗色變數宣告([data-theme='dark'], .dark { --lh-*: ... }),導致暗色模式下配色失真——暗色變數被內聯的明色值蓋死。

buildPresetStyleText 生成的文字結構:

:root {
--lh-primary-color: #2B4C7C;
/* ...預設的全部 vars */
}
[data-theme='dark'],
.dark {
--lh-primary-color-bg: color-mix(in srgb, var(--lh-primary-color) 25%, transparent);
--lh-selected-color: color-mix(in srgb, var(--lh-primary-color) 40%, #fff);
/* 深主色預設額外提亮主色 */
}

暗色塊在 :root 塊之後、同特異性下後者勝——所以淺主色預設在 :root 裡宣告的 --lh-selected-color 明色值不會洩漏進暗色。暗色塊的內容由 buildPresetDarkVars 推導。

三個規則:

  1. 無條件重定義兩個變數(用 var() 引用,在套用元素上求值,跟隨當前主色):

    • --lh-primary-color-bg: color-mix(in srgb, var(--lh-primary-color) 25%, transparent)
    • --lh-selected-color: color-mix(in srgb, var(--lh-primary-color) 40%, #fff)
  2. 深主色提亮:若 --lh-primary-color 相對暗色頁面底 #1A1A2E(DARK_PAGE_BG)對比度小於 3(DARK_PRIMARY_CONTRAST_THRESHOLD),判定為深主色,額外提亮:

    • --lh-primary-color: color-mix(in srgb, <字面值> 55%, #fff) —— 直接代入字面值,因為 color-mix(var(--lh-primary-color) …) 會形成 CSS 自引用循環
    • --lh-primary-color-hover: color-mix(in srgb, var(--lh-primary-color) 70%, #fff)
    • --lh-primary-color-active: color-mix(in srgb, var(--lh-primary-color) 85%, #000)
    • --lh-color-on-primary: #101410(INK_ON_LIGHTENED_PRIMARY)—— 提亮後白字對比度反而不足 3:1,改墨字
  3. 對比度計算用 getContrastRatio(getRelativeLuminance(primary), getRelativeLuminance(DARK_PAGE_BG))。非 hex 值(含 var() 的動態值)傳回 undefined,按「無法判定」處理——所以預設 vars 裡的主色必須是字面 hex。

改內建主題的主色時,兩邊要同步改:

  1. src/theme/themes/index.ts 裡預設的 vars['--lh-primary-color'](JS 執行時注入用)
  2. styles/theme/common/variables.scss 裡的 $lh-primary-color(元件 SCSS 計算用)

tests/unit/theme.spec.ts 有一致性回歸,跑 pnpm test 會攔不同步。

  • 不能改控制項高度:高度令牌 --lh-control-height-* 尚未建立(各控制項高度不統一,記錄在「高度令牌專項」待後續 changeset)。
  • 不能改元件結構:主題只換令牌值,版面配置與 DOM 結構不變。
  • 花紋是獨立系統:見 國風花紋。
青花瓷cerblue · Ceramic Blue

青花瓷釉下彩,清雅素净

选中项主色底文字
--lh-primary-color
#2B4C7C
--lh-primary-color-hover
#3D649E
--lh-primary-color-active
#1F3A5F
--lh-primary-color-bg
#E8EDF3
--lh-primary-color-border
#D4DCE8
松花绿songhua · Pine Green

竹林修竹,生机勃勃

选中项主色底文字
--lh-primary-color
#5B8C5A
--lh-primary-color-hover
#7BB079
--lh-primary-color-active
#4F8F4C
--lh-primary-color-bg
#E8F5E8
--lh-primary-color-border
#D0E8CF
--lh-color-on-primary
#101410
--lh-selected-color
#456F44
朱砂zhusha · Vermilion

传统印章朱砂红,喜庆庄重

选中项主色底文字
--lh-primary-color
#B23A3A
--lh-primary-color-hover
#C45050
--lh-primary-color-active
#9A3030
--lh-primary-color-bg
#FDF0F0
--lh-primary-color-border
#F0D8D8
水墨shuimo · Ink Wash

水墨丹青,素雅极简

选中项主色底文字
--lh-primary-color
#2C2C2C
--lh-primary-color-hover
#5A5A5A
--lh-primary-color-active
#1A1A1A
--lh-primary-color-bg
#F5F5F5
--lh-primary-color-border
#E0E0E0
藤黄golden · Golden Autumn

深秋枫黄,温暖金黄

选中项主色底文字
--lh-primary-color
#B8860B
--lh-primary-color-hover
#D4A843
--lh-primary-color-active
#A87F0A
--lh-primary-color-bg
#FBF6EA
--lh-primary-color-border
#F0E8D0
--lh-color-on-primary
#1E1A16
--lh-selected-color
#82600A

Copyright © 2024-2026 FengMo WenXue