LeCodesdocs

Классы стилей — именованные состояния с каскадом

Класс стиля — это именованное состояние стиля, которое ты объявляешь инлайн и переключаешь из кода — как onPressed, но с любым именем и управляемое тобой: выбрано, отмечено, активно, развёрнуто. Объявляется блоком с префиксом $ внутри .style(), переключается через прокси el.class, и — то, что делает составные контролы дешёвыми, — класс, установленный на элементе, каскадирует на всех его потомков, зажигая и их одноимённые $-блоки.

Обзор

Кнопка «избранное»: один переключатель перестилизует контейнер, иконку и подпись — каждая часть объявляет свою реакцию, ничего не перерендеривается:

TypeScript
const fav = UIButton(
  UIImage(assetIcon("lucide:heart")).style({ width: 18, height: 18,
    tintColor: "#8a919e", $fav: { tintColor: "#ff453a" } }),
  UIText("Favorite").style({ color: "#8a919e", $fav: { color: "#ff453a" } }),
).style({ height: 36, px: 12, gap: 6, borderRadius: 18,
          bgColor: "#17181c", $fav: { bgColor: "#2a181a", duration: 150 } })

fav.onClick(ev => ev.target.class.fav = !ev.target.class.fav)

Объявление: блоки $name

Ключ с префиксом $ внутри любого вызова .style() объявляет класс; блок держит переопределения плюс необязательный переход:

TypeScript
el.style({
  bgColor: "#151515",
  $selected: { bgColor: "#1d2b45", duration: 150 },   // duration/delay (мс) анимируют смену
})

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

Управление: el.class

el.class — прокси с тем же контрактом, что и el.style:

TypeScript
el.class.selected                    // чтение: активен ли на ЭТОМ элементе? → boolean
el.class.selected = true             // активировать (false деактивирует)
el.class.open = !el.class.open      // переключить
el.class.done = () => sig.value      // реактивная привязка — переприменяется при смене сигнала
el.class({ checked: true, done: () => sig.value })   // пакетная форма: возвращает элемент, чейнится
Object.keys(el.class)                // активные имена классов элемента
  • Имя работает с ведущим $ или безel.class.checked и el.class.$checked эквивалентны (удобно при копировании ключа из объявления).
  • Активный набор запоминается на элементе — класс остаётся активным при закрытии и повторном открытии экрана.
  • Реактивная форма привязывает класс к сигналу; ручное переключение и привязка — один механизм.

Каскад

Класс, установленный на элементе, активен и на всех его потомках — их одноимённые $-блоки тоже зажигаются, как класс .dark на <body> в CSS. Это паттерн для любого составного контрола: части объявляют свои реакции, контейнер несёт один флаг.

Кнопка таб-бара с подсветкой, привязанной к активному индексу пейджера, — иконка и подпись следуют одной привязке на кнопке:

TypeScript
const activeTab = signal(0)
pager.onSelect(i => activeTab.value = i)      // тапы И свайпы двигают подсветку

const TabButton = (label: string, icon: string, i: number) => UIButton(
  UIImage(assetIcon(icon)).style({ width: 24, height: 24,
    tintColor: "#98A29C", $active: { tintColor: "#15A34A" } }),
  UIText(label).style({ fontSize: 12, mt: 4, color: "#98A29C", $active: { color: "#15A34A" } }),
)
  .style({ flex: 1, flexDirection: "column", py: 6 })
  .class({ active: () => activeTab.value === i })     // ОДНА привязка ведёт каждую часть
  .onClick(() => pager.select(i))

Правила каскада:

  • Чтения отражают только собственные классы элемента — el.class.active на иконке выше равен false, даже пока каскад кнопки её стилизует.
  • Опции отказа нет: пока класс есть у предка, el.class.name = false у потомка сбрасывает только его собственный флаг.
  • Каскад останавливается на presentable-корнях — размещённые экраны (страницы UIPager) и виджеты не наследуют от того, что их содержит.

$pressed и $focused

Два имени классов зарезервированы и переключаются системой: $pressed, пока элемент удерживается, $focused, пока инпут в фокусе. В отличие от onPressed/onFocused они каскадируют — дети кнопки могут перестилизоваться во время её нажатия:

TypeScript
UIButton(
  UIImage(icon).style({ $pressed: { opacity: 0.5 } }),
  UIText("Buy").style({ $pressed: { color: "#999" } }),
).style({ $pressed: { transform: "scale(0.97)" } })

Предпочитай $pressed/$focused в новом коде — один словарь для элемента и его детей. onPressed/onFocused остаются формами на элемент, без каскада; они никогда не зажигаются от предка — то, что нужно на вложенных интерактивах (кнопка внутри кликабельной карточки не должна «нажиматься» вместе с карточкой).

Приоритет

Когда два состояния задают одно свойство, побеждает более позднее и более интерактивное, от низшего к высшему:

TypeScript
base style  <  $classes (later-declared beats earlier)  <  $pressed / $focused  <  onPressed / onFocused

Отклик нажатия/фокуса всегда бьёт остальные классы, пока элемент нажат или в фокусе, независимо от порядка объявления — поэтому отмеченная кнопка всё ещё показывает нажатие:

TypeScript
UIButton(UIText("Save")).style({
  bgColor: "#222",
  $checked: { bgColor: "#2a7" },        // показывается, пока отмечено
  $pressed: { bgColor: "#195" },        // побеждает, пока палец внизу, даже когда отмечено
})
Реактивный ярлык

одному вычисляемому значению класс не нужен — любое примитивное свойство стиля принимает привязку () => value напрямую (bgColor: () => sel.value ? "#FF4032" : "#333"). Тянись к классу, когда хочешь именованный, переиспользуемый блок переопределений — особенно такой, на который через каскад реагируют несколько элементов.

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

  • Стилизация — словарь свойств, из которого сделаны блоки классов.
  • Тема — переменные на всё приложение (тема = на всё приложение, классы = состояния на элемент).
  • Сигналы — реактивность за привязками () => value.
  • Интерактивные элементыonPressed/onFocused, ripple, механика нажатия.