コンテンツにスキップ

テーマシステム概要

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 の部分木に注入し、コンポーネントが再コンパイルなしでスキンを切り替えられるようにすることです。

1. プリセットテーマ(すぐに使える)

Section titled “1. プリセットテーマ(すぐに使える)”

5 つの内蔵プリセット:青花瓷(せいがじ)/ 松花绿(しょうかりょく)/ 朱砂(しゅしゃ)/ 水墨(すいぼく)/ 藤黄(とうおう)。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"> に注入

詳細はテーマプリセットを参照。

2. ランタイムカスタマイズ(themeVars)

Section titled “2. ランタイムカスタマイズ(themeVars)”

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 のテクニックです:最初の 5 つのリテラルの自動補完を保ちつつ、registerTheme({ name: 'my-brand', ... }) で任意の文字列を渡せるようにします。未登録の名前は getThemePreset / getThemeVars で undefined を返します。

これはテーマシステムで最も重要な制約で、一度罠を踏んでいます:

// ✅ 正しい:<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 が導出します。

3 つのルール:

  1. 2 つの変数を無条件に再定義(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