Обзор системы тем
Система тем lh-ui имеет два источника — об этом нужно сказать с самого начала:
| Источник | Содержимое | Способ потребления |
|---|---|---|
styles/theme/common/var.scss + var-dark.scss |
CSS custom properties (--lh-*) |
В компонентных SCSS var(--lh-*), вычисляется в runtime |
styles/theme/common/variables.scss |
SCSS-переменные ($lh-*) |
Запекаются на этапе компиляции в конкретные значения; используются для вычислений (цветовые функции, компоновка теней и т. п.) |
src/theme/themes/index.ts |
Отображения vars пяти пресетов |
Инъекция в runtime в <style> (ConfigProvider / ThemeStudio) |
Компоненты ориентируются только на CSS-переменные. Смысл существования JS-стороны системы тем в том, чтобы в runtime внедрить набор design-токенов в некоторое поддерево DOM и позволить компонентам менять облик без повторной компиляции.
Два пути
Заголовок раздела «Два пути»1. Пресеты тем (работают из коробки)
Заголовок раздела «1. Пресеты тем (работают из коробки)»Пять встроенных пресетов: 青花瓷 (сине-белый фарфор) / 松花绿 (зелёный сосны) / 朱砂 (киноварь) / 水墨 (тушь) / 藤黄 (gamboge). Слой 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. Настройка в runtime (themeVars)
Заголовок раздела «2. Настройка в runtime (themeVars)»ConfigProvider принимает themeVars с сокращёнными ключами (префикс --lh- добавляется автоматически):
<lh-config-provider :theme-vars="{ 'primary-color': '#1677ff' }"> <App /></lh-config-provider>Подробнее см. Настройка в runtime.
Пять runtime-примитивов
Заголовок раздела «Пять runtime-примитивов»Экспортируются из 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>Конструкция (string & {}) в конце LhThemePresetName — это приём в TS: он сохраняет автодополнение первых пяти литералов и одновременно разрешает registerTheme({ name: 'my-brand', ... }) принимать любую строку. Незарегистрированные имена возвращают undefined из getThemePreset / getThemeVars.
Ограничение инъекции: писать только <style>, не встроенный style
Заголовок раздела «Ограничение инъекции: писать только <style>, не встроенный style»Это самое важное ограничение системы тем — однажды в него уже попали:
// ✅ Правильно: создаётся элемент <style>, специфичность 0,1,0const el = document.createElement('style')el.id = LH_PRESET_THEME_STYLE_IDel.textContent = buildPresetStyleText(vars)document.head.appendChild(el)
// ❌ Неправильно: запись во встроенный style на documentElement, специфичность 1,0,0,0Object.assign(document.documentElement.style, vars)Почему: специфичность встроенного style выше, чем у любой таблицы стилей, и она перекрывает все объявления тёмных переменных в var-dark.scss ([data-theme='dark'], .dark { --lh-*: ... }), из-за чего цветовая палитра в тёмной теме искажается — тёмные переменные перекрываются светлыми значениями из встроенного style.
Структура текста, генерируемого 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, и при равной специфичности побеждает последний — поэтому светлое значение --lh-selected-color, объявленное в :root у пресетов со светлым основным цветом, не просачивается в тёмную тему. Содержимое тёмного блока выводится функцией buildPresetDarkVars.
Выведение тёмных переменных в buildPresetDarkVars
Заголовок раздела «Выведение тёмных переменных в buildPresetDarkVars»Три правила:
-
Безусловно переопределяются две переменные (через ссылки
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)
-
Осветление тёмного основного цвета: если контрастность
--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, поэтому текст становится чёрным
-
Контрастность вычисляется как
getContrastRatio(getRelativeLuminance(primary), getRelativeLuminance(DARK_PAGE_BG)). Не-hex значения (включая динамические значения сvar()) возвращаютundefinedи трактуются как «невозможно определить» — поэтому основной цвет в vars пресета должен быть литеральным hex.
Синхронизация двух источников
Заголовок раздела «Синхронизация двух источников»При изменении основного цвета встроенной темы менять нужно в двух местах:
vars['--lh-primary-color']пресета вsrc/theme/themes/index.ts(для инъекции в runtime на JS)$lh-primary-colorвstyles/theme/common/variables.scss(для вычислений в компонентных 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
Связанные документы
Заголовок раздела «Связанные документы»- Пресеты тем — подробности о пяти встроенных пресетах
- Токены дизайна — полная таблица: 185 светлых + 118 тёмных
- Настройка в runtime — themeVars / componentTheme / patternComponents
- Тёмная тема — механизм и ловушка запекания на этапе компиляции
- Узоры в китайском стиле — 6 встроенных текстур
Copyright © 2024-2026 FengMo WenXue