Стилизация UI
Система стилей, общая для каждого UI-элемента: один метод .style(), один словарь свойств,
раскладка только флексбокс (движок Yoga — без CSS-grid, без блочного потока), единицы в логических px.
Эта страница — канонический список свойств стиля и форм значений; специфичные для элемента добавки
(objectFit, onPressed, scrollDirection, …) живут на странице самого элемента.
Обзор
const card = UIColumn([
UIText("Title").style({ color: "white", fontSize: 20, fontWeight: 700 }),
])
.style({ bgColor: "#111", borderRadius: 16, p: 16, gap: 8 }) // слияние — чейнится
.style({ onLandscape: { flexDirection: "row" } }) // адаптивное переопределение
card.style.opacity = 0.5 // мутация одного свойства (горячие пути)
card.animateTo({ bgColor: "#333", duration: 200 }) // твин к новым значениям (мс)Три способа задать стили
el.style({ ... }): this // СЛИВАЕТ в текущий стиль (не заменяет); чейнится
el.style.prop = value // прямая запись одного свойства после создания (горячие пути)
el.style.prop // прочитать последнее заданное тобой значениеСлияние .style() означает, что поздние вызовы переопределяют только упомянутые ключи:
UIText("Hi").style({ color: "white" }).style({ fontSize: 20 }) // применяются обаИспользуй прямую мутацию для покадровых обновлений, напр. el.style.transform = translateY(${y}px)``.
Любое свойство с примитивным значением также принимает функцию () => value — реактивную
привязку, которая переприменяет себя, когда меняется прочитанный ею сигнал:
el.style({ bgColor: () => (selected.value ? "#FF4032" : "#333") })Анимация: animateTo / animateFrom
el.animateTo({ opacity: 0, bgColor: "#000", duration: 300, delay?: 0, commit?: true }): this
el.animateFrom({ opacity: 0, duration: 300, delay?: 0 }): this // от заданных значений → текущиеduration/delay— миллисекунды.animateToтвинит от текущих значений к заданным и сразу коммитит цели в стиль элемента (чтобы чтения и поздние слияния видели финальное состояние). Передайcommit: false, чтобы анимировать без записи стиля — напр. затухание оверлея прямо перед.hide().animateFrom— входная анимация: она прыгает к заданным значениям и анимирует обратно к текущему стилю элемента. Она никогда не меняет сохранённый стиль.
тип опций также допускает layer?: number — лазейку, нацеленную на внутренний слой стиля (механизм
за стилями onPressed/ориентации). Не часть поддерживаемой поверхности.
Для твинов свободных значений (числа, которые применяешь сам) используй animate() — см.
animate.
Словарь свойств
Раскладка — только флексбокс. Каждый элемент — флекс-контейнер, position: relative,
flexDirection: column по умолчанию (UIRow переключает на row).
Раскладка контейнера (UIRow / UIColumn / UIScrollable / экраны / кнопки / виджеты)
flexDirection: "row" | "column"
justifyContent: "flex-start" | "center" | "flex-end" | "space-between" | "space-evenly"
alignItems: "flex-start" | "center" | "flex-end" | "stretch" // по умолчанию "stretch"
gap: UIValue
flexWrap: "nowrap" | "wrap" | "wrap-reverse"Флекс-ребёнок (все элементы)
flex: number | string
flexGrow: number // по умолчанию 0 — без него ничего не растёт
flexShrink: number // по умолчанию 0 — и ничего не сжимается
flexBase: UIValue | "auto" // внимание: flexBase, не flexBasis
alignSelf: "flex-start" | "center" | "flex-end" | "stretch"
aspectRatio: numberРазмер и позиция (все элементы)
width, height: UIValue | "auto"
minWidth, maxWidth,
minHeight, maxHeight: UIValue | "auto"
position: "relative" | "absolute" | "static" // по умолчанию "relative"
top, left, bottom, right: UIValue | safe-area keyword // смещения; % разрешён
boxSizing: "border-box" | "content-box" // по умолчанию "border-box"Отступы и поля (все элементы)
Сокращения и полные формы взаимозаменяемы (p ≡ padding, pt ≡ paddingTop, …):
p (padding) px (paddingHorizontal) py (paddingVertical)
pt pb pl pr // на сторону
m (margin) — also accepts "auto" mx (marginHorizontal) my (marginVertical)
mt mb ml mr — also accept "auto"mx: "auto" центрирует элемент фиксированной ширины; одиночное поле "auto" толкает его к дальней
стороне.
Фон и рамка (рисуемые элементы)
Доступны на контейнерах, UIScreen, UIButton, UIInput, UIVideo. У UIText, UIImage и
UISpacer есть только bgColor (плюс собственный числовой borderRadius у UIImage).
bgColor: Color // ≡ backgroundColor; все элементы
bgImage: string | FetchResponse | File // ≡ backgroundImage; только декор
bgSize: "cover" | "contain" | "tile" // ≡ backgroundSize
bgGradient: string // один+ слоёв linear-gradient()/radial-gradient() через запятую
border: string | number // "1px solid #333" или голая ширина
borderWidth: number
borderColor: Color
borderTop / borderRight / borderBottom / borderLeft // сокращения на сторону
borderTopWidth / borderTopColor / … // полные формы на сторону
borderRadius: UIValue | string // строка = на угол "0 0 20 20"
borderTopLeftRadius / borderTopRightRadius /
borderBottomLeftRadius / borderBottomRightRadius: UIValueСлои фона рисуются снизу вверх: bgColor → bgImage → bgGradient — поэтому градиент поверх
изображения делает привычный затемняющий scrim для защиты текста:
.style({ bgImage: photo, bgSize: "cover",
bgGradient: "linear-gradient(to top, rgba(0,0,0,0.7), transparent)" })Градиенты (bgGradient)
bgGradient принимает CSS-строку градиента — linear-gradient() или radial-gradient(). Цветовые
стопы принимают любой Color (hex, rgb()/rgba(), именованный, transparent) с необязательными
позициями %.
Линейный — необязательное направление (to <side> / <angle>deg, по умолчанию to bottom =
180deg), затем ≥2 стопа:
.style({ bgGradient: "linear-gradient(135deg, #FF4032, #7B2FF7)" })
.style({ bgGradient: "linear-gradient(to right, #000 0%, #333 60%, #fff 100%)" })Радиальный — необязательный префикс [<shape> || <extent>]? [at <position>]?, затем ≥2 стопа:
.style({ bgGradient: "radial-gradient(#7B2FF7, #0a0a1a)" }) // по умолчанию: ellipse, farthest-corner, at center
.style({ bgGradient: "radial-gradient(circle at 50% 40%, #7B2FF7, #0a0a1a)" }) // circle, вне центра
.style({ bgGradient: "radial-gradient(ellipse closest-side at top left, #fff, #101014)" })
.style({ bgGradient: "radial-gradient(circle 80px at center, #fff, #101014)" }) // явный радиусРазмер радиального — это ключевое слово extent или явные радиусы. Оба, плюс форма и позиция, учитываются на каждой поверхности (браузерный предпросмотр, iOS, headless-рендерер):
| Поле | Значения | По умолчанию |
|---|---|---|
| shape | circle | ellipse |
ellipse |
| extent | closest-side | closest-corner | farthest-side | farthest-corner |
farthest-corner |
| явные радиусы | circle <len> или ellipse <len-or-%>{2} — длины это px/em/vw/calc() или % от оси бокса |
— |
at <position> |
ключевые слова (center, top, left, bottom right, …) или % (20% 80%) |
center |
браузерный предпросмотр рендерит каждую CSS-радиальную форму точно (это сырой CSS). На
устройстве (iOS) и в headless-рендерере поля выше разрешаются 1:1. Более редкие CSS-формы вне
этой таблицы (напр. позиции из 4 значений at left 10% top 20%) откатываются там к центрированному
дефолту, тогда как предпросмотр остаётся точным. conic-gradient() не поддерживается.
Наслоение нескольких градиентов. Раздели несколько градиентов запятыми (как CSS
background-image), чтобы сложить их — первый в списке рисуется сверху, поэтому веди теми, у кого
есть transparent-области:
.style({ bgColor: "#0a0a1a", bgGradient:
"radial-gradient(circle at 15% 20%, #7B2FF7, transparent 60%), " +
"radial-gradient(circle at 85% 80%, #2FB0F7, transparent 55%)" })Линейные и радиальные слои можно смешивать, каждый учитывает полный синтаксис выше, а стек композится на каждой поверхности (браузерный предпросмотр, iOS, headless-рендерер) — обычный способ строить мягкие мульти-свечения или «авроровые» фоны.
bgImage — декор за детьми. Изображение, которое является контентом, — это UIImage — см.
content.md.
Всё остальное (все элементы)
opacity: number | string // 0..1
transform: string // строка в стиле CSS, напр. `translateY(12px)` — отлично для жестов
display: "none" | "flex" // "none" убирает из раскладки
overflow: "visible" | "hidden" // по умолчанию "hidden" — дети обрезаются
pointerEvents: "all" | "none" // рисуемые элементы; "none" пропускает тапы насквозьЗначения и единицы (UIValue)
16 // голое число = логические px (единица по умолчанию везде)
"50%" // процент от родителя
"50vw" "50vh" // ширина/высота вьюпорта
"50vmin" "50vmax"
"1.5em" // × СОБСТВЕННЫЙ fontSize элемента (по умолчанию 14) — наследования стиля НЕТ
"calc(100vw - 32px)"
"min(...)" "max(...)" "clamp(min, val, max)"emразрешается относительно собственногоfontSizeэлемента, никогда родительского — задайfontSizeна том же элементе, иначеemзначит «относительно 14px».calc()/min()/max()/clamp()комбинируют px, единицы вьюпорта,emи ключевые слова safe-area и вкладываются свободно — но не поддерживают%.
width: "calc(100% - 20px)" // ✗ % не может быть внутри calc
width: "calc(100vw - 20px)" // ✓ используй единицы вьюпортаЦвета (только UI-стили)
Строки цвета UI парсятся UI-движком и принимают больше, чем ColorInput со стороны движка
(соглашения):
"#f33" "#f33c" "#ff3333" "#ff3333cc" // hex, 3/4/6/8 цифр
"rgb(255, 51, 51)" "rgba(255, 51, 51, 0.8)"
0xff3333 // упакованный int
// именованные — ровно этот набор, ничего больше:
"white" "black" "red" "green" "blue" "yellow" "orange" "purple"
"gray" "cyan" "magenta" "brown" "transparent" "clear"этот парсер существует только для UI-стилей. API 2D/3D (Sprite.color, Material, фон Scene2D)
принимают только hex-строки и упакованные int — rgba(...) и именованные цвета там молча становятся
непрозрачным чёрным.
Безопасные зоны
Ключевые слова, разрешающиеся в безопасные отступы устройства (вырез, home-индикатор). Точный набор:
safe-top safe-bottom safe-left safe-right
safe-top-comfort safe-bottom-comfort safe-left-comfort safe-right-comfort
safe-all safe-all-comfort // только на сокращениях p / m (padding / margin)Где они принимаются:
- padding на сторону (
pt,paddingBottom, …) — ключевое слово соответствующей стороны;p/paddingпринимаетsafe-all - margin на сторону и
m/margin— тот же набор (наряду с"auto") - смещения позиции
top/left/bottom/right— ключевое слово соответствующей стороны - внутри
calc()/min()/max()— канонический паттерн для шапки:
.style({ pt: "max(safe-top, 24px)" }) // не меньше 24px даже на устройствах без вырезовВарианты -comfort никогда не схлопываются в ноль: они гарантируют комфортное пространство для
дыхания (≥ 12 логических px по вертикали по умолчанию), даже когда сырой отступ равен 0 — используй
их для контента, который иначе касался бы края экрана на старых устройствах.
Адаптивность: onLandscape / onPortait
Любой объект стиля может вкладывать переопределения ориентации; они сливаются сверху, когда устройство в этой ориентации, и снимаются, когда она меняется:
screen.style({ flexDirection: "column", p: 16, onLandscape: { flexDirection: "row", p: 32 } })ключ портрета действительно пишется onPortait (без второй «r») — это реальное имя API.
Классы стилей
Класс стиля — это именованное состояние стиля, которое ты объявляешь инлайн и переключаешь из
кода — как onPressed, но с любым именем и управляемое тобой, а не системой. Используй для стойких
состояний: выбрано, отмечено, активно, развёрнуто.
Объяви класс ключом с префиксом $ внутри .style(); его блок держит переопределения (плюс
необязательный переход duration / delay). Активируй или деактивируй его на элементе:
const toggle = UIButton([ UIText("Dark mode") ]).style({
bgColor: "#222",
$checked: { bgColor: "#FF4032", duration: 150 }, // объявляется как onPressed
})
toggle.onClick(() => toggle.toggleClass("checked")) // переключить
// или задать явно:
toggle.setClass("checked", true)
toggle.hasClass("checked") // → trueel.setClass(name, enabled): this // активировать (true) / деактивировать (false) класс на этом элементе
el.toggleClass(name): this // переключить
el.hasClass(name): boolean // активен ли он сейчас на этом элементе?- Имя даётся с ведущим
$или без —setClass("checked", …)иsetClass("$checked", …)эквивалентны. - Активация на элемент: перестилизует только тот элемент, на котором вызвано, не его детей.
- Активный набор запоминается на элементе, поэтому класс остаётся активным при закрытии и повторном открытии экрана.
- Переход (
duration/delay, мс) заставляет смену анимироваться, на каждой платформе.
Приоритет — когда два состояния задают одно свойство, побеждает более позднее и более интерактивное, в таком порядке (низший → высший):
base style < $classes (later-declared beats earlier) < onPressed / onFocusedТак класс бьёт базовый стиль, класс, объявленный позже, бьёт объявленный раньше, а onPressed (или
onFocused) всегда бьёт класс, пока элемент нажат/в фокусе. Пример — отмеченная кнопка, которая всё
ещё показывает отклик на нажатие:
UIButton([ UIText("Save") ]).style({
bgColor: "#222",
$checked: { bgColor: "#2a7" }, // показывается, когда отмечено
onPressed: { bgColor: "#195" }, // побеждает, пока палец внизу, даже когда отмечено
})чтобы привязать класс к сигналу вместо ручного переключения, свойство
примитивного стиля уже принимает привязку () => value (см. выше) — тянись к классу, когда хочешь
именованный, переиспользуемый блок состояния, а не одно вычисляемое значение.
Дефолты, которые удивляют
flexShrink: 0— элементы не сжимаются под размер;UIScrollableв колонке переполняется вместо прокрутки, пока он (и любые оборачивающие предки) не получатflexShrink: 1.flexGrow: 0— ничего не растёт вдоль главной оси без спроса; неявных мин-размеров тоже нет.alignItems: "stretch"— дети заполняют поперечную ось по умолчанию; задай размер илиalignSelf, чтобы отказаться.overflow: "hidden"— дети обрезаются по боксу родителя.position: "relative",boxSizing: "border-box"(width/height включают отступ + рамку).- Фон экрана чёрный, а
fontSizeпо умолчанию 14 — всегда задавайbgColorиcolorтекста явно.
Логические px и дизайн-канвас
Все единицы — логические px (iOS pt / Android dp), никогда физические — fontSize: 16 выглядит
одинаково на каждом устройстве. Проектируй под канвас телефона шириной ~360–430 (390 — хороший
дефолт) и высотой ~670–930; вертикальное пространство скудно на экранах класса SE, поэтому длинный
контент — в UIScrollable или прокручиваемом экране.
Подводные камни
display: "grid" // ✗ только флексбокс (Yoga)
lineHeight: 1.5 // ✗ число это px (= 1.5px); множитель это "1.5em"
node.style({ width: "calc(100% - 20px)" }) // ✗ без % внутри calc — используй 100vwСмотрите также
- Модель элементов UI — фабрики, дети, ссылки,
onLayout. - Контейнеры — где применяются свойства раскладки контейнера.
- Элементы контента — стили текста,
objectFit,tintColor. - animate — твины свободных значений и функции сглаживания.