콘텐츠로 이동

테마 시스템 개요

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의 어떤 부분 트리에 주입하여, 컴포넌트가 재컴파일 없이 스킨을 바꿀 수 있게 하는 것입니다.

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">에 주입

자세한 내용은 테마 프리셋을 참고하세요.

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>만 허용, 인라인 style 금지

섹션 제목: “주입 제약: <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가 도출합니다.

buildPresetDarkVars의 어두운색 도출

섹션 제목: “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-*가 아직 설정되지 않았습니다(각 컨트롤의 높이가 제각각이며, “높이 토큰 전문 작업”에 기록되어 향후 체인지셋을 기다리는 중).
  • 컴포넌트 구조를 바꿀 수 없습니다: 테마는 토큰 값만 바꾸고, 레이아웃과 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