LeCodesdocs

Тема — переменные стиля на всё приложение

Одна таблица переменных на всё приложение, разрешаемая вживую ядром UI. theme(values) сливает переменные в неё и возвращает типизированные аксессоры, чьи значения и есть строки var(); любое значение стиля может ссылаться на переменную, а повторный вызов theme() перестилизует работающий UI на месте — тёмная тема, смена бренда или персональный акцент — это ещё один вызов, а не пересборка. Эта страница — канонический дом theme(); словарь свойств, в которые переменные подставляются, — в styling.md.

Обзор

Стандартная форма — маленький модуль токенов, который импортирует каждый экран: палитра в theme(), шкалы — обычные константы:

TypeScript
// theme.ts — определи один раз, экспортируй аксессоры
const palette = {
  bg: "#F4F6F5", card: "#FFFFFF", border: "#E4E8E6",
  text: "#131A17", muted: "#606B65",
  accent: "#15A34A", accentSoft: "#E7F6ED", onAccent: "#FFFFFF",
}

// Системные ключи: цвет текста по умолчанию + чем красят себя кит-компоненты (панель UITabs).
theme({ color: palette.text, primaryColor: palette.accent, mutedColor: palette.muted })

export const colors: { [K in keyof typeof palette]: string } = theme(palette)
export const font = {    // шкала типографики — распыляй в стили: .style({ ...font.h2 })
  h2: { fontSize: 22, fontWeight: 700 }, body: { fontSize: 16 },
  small: { fontSize: 14 }, tiny: { fontSize: 12, fontWeight: 500 },
}
TypeScript
// любой экран
import { colors, font } from './theme'

const card = UIColumn(
  UIText("Total").style({ ...font.small, color: colors.muted }),
  UIText("$1,240").style({ ...font.h2 }),
).style({ bgColor: colors.card, border: `1px solid ${colors.border}`, borderRadius: 16, p: 16 })
TypeScript
// позже, откуда угодно — каждый стиль, ссылающийся на токен, перекрашивается на месте
theme({ bg: "#0C0E13", card: "#161A23", text: "#EEF1F8", border: "#2A3040" })   // тёмная тема

colors.card — это буквально строка "var(--card)" — хост разрешает её в момент применения стиля. Шкалы отступов и типографики остаются обычными TS-константами: переменная темы — для того, что должно уметь меняться, пока приложение работает.

Note

экспорт аксессоров аннотирован { [K in keyof typeof palette]: string } не случайно. theme() выводит литеральные типы ("var(--accent)"), и хелпер вида (color = colors.accent) иначе сузил бы свой параметр до этого одного токена.

Определение переменных

TypeScript
theme(values): accessors      // СЛИВАЕТ в живую таблицу; неупомянутые ключи сохраняют значения
  • Строки проходят как есть — цвета, имена шрифтов, целые выражения ("max(safe-top, 24px)").
  • Числа — длины (логические px).
  • null удаляет ключ (ручки комфорта сбрасываются к встроенным дефолтам).
  • Env-имена (safe-top…, vw/vh/vmin/vmax) — факты хоста, никогда не ключи темы — записи пропускаются с предупреждением в консоли.
  • Один нестилевой ключ: replaceTransition ниже.

Каждый вызов возвращает аксессоры для определённых им ключей; возвращённый объект — всё, что нужно экспортировать из модуля токенов.

Чтение переменных

TypeScript
label.style({ color: T.accent })                     // аксессор — T.accent === "var(--accent)"
label.style({ color: "var(--accent)" })              // сырая строковая форма — то же самое
icon.style({ tintColor: "var(--accent, #0A84FF)" })  // фолбэк действует, пока ключ не задан
list.style({ pl: "calc(var(--comfort-left) * 2)" })  // компонуется в calc()/min()/max()

var() работает в любом значении UI-стиля и больше нигде — что это исключает, см. Подводные камни. var() для ключа, который никогда не определялся, разрешается в свой фолбэк или в ничто.

Системные переменные

Некоторые ключи читает сам SDK, а не только твои стили. Они заранее типизированы как статические свойства глобала — theme.color, theme.primaryColor, … — так что модуль токенов им не нужен.

Текст по умолчанию — два ключа управляют каждым UIText, который не задал собственное значение, так что светлая тема — это одна строка вместо color: "black" на каждой подписи:

TypeScript
theme({ color: "#1C1C1E", fontFamily: font("manrope") })   // весь нестилизованный текст следует

Не заданные, они равны дефолтам хоста (белый текст, шрифт хоста). Собственные color / fontFamily элемента всегда побеждают. Фон экрана остаётся у каждого экрана свой (bgColor) — стилизуй его явно.

Кит-компонентыUITabs стилизует свою панель целиком через переменные темы (с тёмными фолбэками, так что приложение без темы всё равно выглядит правильно):

Ключ Управляет
primaryColor иконка + подпись активного таба (и общий «акцент приложения» для кит-компонентов)
mutedColor иконка + подпись неактивного таба / вторичный контент
tabbarBg поверхность таб-бара
tabbarBorder верхняя волосяная линия таб-бара
screenBg фон по умолчанию за экранами табов
badgeColor точка бейджа / пилюля счётчика

Ручки комфорта"comfort-top" / "comfort-bottom" / "comfort-left" / "comfort-right" настраивают комфортные токены безопасных зон (только простые длины):

TypeScript
theme({ "comfort-left": 20, "comfort-right": 20 })   // гаттер страниц этого приложения — 20
Note

голый токен стиля (pt: "comfort-top") применяет формулу безопасной зоны; var(--comfort-top) читает сырое значение ручки (по умолчанию 12) для ручной композиции.

Живая смена темы

Поскольку каждый var() переразрешается на месте, вызов theme() перестилизует то, что сейчас на экране, — без пересборки экрана, без протаскивания пропсов, без точки вызова, которую можно забыть. Поэтому таблица — правильный дом для всего, что решается в рантайме:

TypeScript
// Тёмная тема: второй вызов.
const setDark = (dark: boolean) => theme(dark ? darkPalette : lightPalette)

// Акцент с сервера: выведи несколько переменных из одного hex и перезапиши их.
// Каждый стиль, где написано var(--accent) / var(--accentSoft), следует — включая экраны,
// которые уже смонтированы.
const applyBrand = (hex: string | null) =>
  theme(hex ? { accent: hex, accentSoft: soften(hex) } : { accent: "#15A34A", accentSoft: "#E7F6ED" })

Записи применяются сразу, когда бы ни случились — между вызовом theme() и экранами, построенными до или после него, нет опасности порядка.

replaceTransition

Один ключ потребляется роутером SDK и не попадает в таблицу переменных: общий для приложения переход по умолчанию для Router.replace (из коробки "none").

TypeScript
theme({ replaceTransition: "fade" })   // любое имя перехода или свой спек; null сбрасывает

Явный Router.replace(screen, { transition }) на вызов всё равно побеждает. Аксессор не возвращается — var(--replaceTransition) не значение стиля. См. screen-router.md.

Подводные камни

TypeScript
// ✗ var() вне UI-стилей — SVG-XML, цвета движка, параметры нативных вью получают буквальную строку
SvgSource(`<circle fill="${colors.accent}" ...>`)      // не рендерит ничего полезного
sprite.color = colors.accent                           // API движка парсят только hex/упакованный int
// ✓ держи сырой объект палитры экспортированным рядом с аксессорами для этих потребителей
SvgSource(`<circle fill="${palette.accent}" ...>`)

// ✗ темизация env-имени — принадлежит хосту, пропускается с предупреждением
theme({ "safe-top": 0 })

// ✗ формула в ручке комфорта — ручки принимают только простые длины
theme({ "comfort-bottom": "max(safe-bottom, 16px)" })
// ✓ пиши формулу инлайн в стиле или как собственную переменную
theme({ tabInset: "max(safe-bottom, 16px)" }); bar.style({ pb: "var(--tabInset)" })

Смотрите также

  • Стилизация — словарь свойств и формы значений, куда подставляются переменные.
  • Классы стилей — именованные состояния на элемент (тема = на всё приложение, классы = на элемент).
  • Шрифтыtheme({ fontFamily: font(...) }), механизм шрифта по умолчанию.
  • UIPager и UITabs — таб-бар, который стилизуют кит-переменные.