Классы стилей — именованные состояния с каскадом
Класс стиля — это именованное состояние стиля, которое ты объявляешь инлайн и переключаешь из
кода — как onPressed, но с любым именем и управляемое тобой: выбрано, отмечено, активно,
развёрнуто. Объявляется блоком с префиксом $ внутри .style(), переключается через прокси
el.class, и — то, что делает составные контролы дешёвыми, — класс, установленный на элементе,
каскадирует на всех его потомков, зажигая и их одноимённые $-блоки.
Обзор
Кнопка «избранное»: один переключатель перестилизует контейнер, иконку и подпись — каждая часть объявляет свою реакцию, ничего не перерендеривается:
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() объявляет класс; блок держит переопределения
плюс необязательный переход:
el.style({
bgColor: "#151515",
$selected: { bgColor: "#1d2b45", duration: 150 }, // duration/delay (мс) анимируют смену
})Каждый элемент в каскаде анимируется по duration собственного блока — переключение это одна
запись, переходы локальны.
Управление: el.class
el.class — прокси с тем же контрактом, что и el.style:
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. Это паттерн для любого составного контрола:
части объявляют свои реакции, контейнер несёт один флаг.
Кнопка таб-бара с подсветкой, привязанной к активному индексу пейджера, — иконка и подпись следуют одной привязке на кнопке:
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 они
каскадируют — дети кнопки могут перестилизоваться во время её нажатия:
UIButton(
UIImage(icon).style({ $pressed: { opacity: 0.5 } }),
UIText("Buy").style({ $pressed: { color: "#999" } }),
).style({ $pressed: { transform: "scale(0.97)" } })Предпочитай $pressed/$focused в новом коде — один словарь для элемента и его детей.
onPressed/onFocused остаются формами на элемент, без каскада; они никогда не зажигаются от
предка — то, что нужно на вложенных интерактивах (кнопка внутри кликабельной карточки не должна
«нажиматься» вместе с карточкой).
Приоритет
Когда два состояния задают одно свойство, побеждает более позднее и более интерактивное, от низшего к высшему:
base style < $classes (later-declared beats earlier) < $pressed / $focused < onPressed / onFocusedОтклик нажатия/фокуса всегда бьёт остальные классы, пока элемент нажат или в фокусе, независимо от порядка объявления — поэтому отмеченная кнопка всё ещё показывает нажатие:
UIButton(UIText("Save")).style({
bgColor: "#222",
$checked: { bgColor: "#2a7" }, // показывается, пока отмечено
$pressed: { bgColor: "#195" }, // побеждает, пока палец внизу, даже когда отмечено
})одному вычисляемому значению класс не нужен — любое примитивное свойство стиля принимает привязку
() => value напрямую (bgColor: () => sel.value ? "#FF4032" : "#333"). Тянись к классу, когда
хочешь именованный, переиспользуемый блок переопределений — особенно такой, на который через
каскад реагируют несколько элементов.
Смотрите также
- Стилизация — словарь свойств, из которого сделаны блоки классов.
- Тема — переменные на всё приложение (тема = на всё приложение, классы = состояния на элемент).
- Сигналы — реактивность за привязками
() => value. - Интерактивные элементы —
onPressed/onFocused, ripple, механика нажатия.