LeCodesdocs

Стилизация UI

Система стилей, общая для каждого UI-элемента: один метод .style(), один словарь свойств, раскладка только флексбокс (движок Yoga — без CSS-grid, без блочного потока), единицы в логических px. Эта страница — канонический список свойств стиля и форм значений; специфичные для элемента добавки (objectFit, onPressed, scrollDirection, …) живут на странице самого элемента.

Обзор

TypeScript
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 })   // твин к новым значениям (мс)

Три способа задать стили

TypeScript
el.style({ ... }): this        // СЛИВАЕТ в текущий стиль (не заменяет); чейнится
el.style.prop = value          // прямая запись одного свойства после создания (горячие пути)
el.style.prop                  // прочитать последнее заданное тобой значение

Слияние .style() означает, что поздние вызовы переопределяют только упомянутые ключи:

TypeScript
UIText("Hi").style({ color: "white" }).style({ fontSize: 20 })   // применяются оба

Используй прямую мутацию для покадровых обновлений, напр. el.style.transform = translateY(${y}px)``.

Любое свойство с примитивным значением также принимает функцию () => valueреактивную привязку, которая переприменяет себя, когда меняется прочитанный ею сигнал:

TypeScript
el.style({ bgColor: () => (selected.value ? "#FF4032" : "#333") })

Анимация: animateTo / animateFrom

TypeScript
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 — входная анимация: она прыгает к заданным значениям и анимирует обратно к текущему стилю элемента. Она никогда не меняет сохранённый стиль.
Note

тип опций также допускает layer?: number — лазейку, нацеленную на внутренний слой стиля (механизм за стилями onPressed/ориентации). Не часть поддерживаемой поверхности.

Для твинов свободных значений (числа, которые применяешь сам) используй animate() — см. animate.

Словарь свойств

Раскладка — только флексбокс. Каждый элемент — флекс-контейнер, position: relative, flexDirection: column по умолчанию (UIRow переключает на row).

Раскладка контейнера (UIRow / UIColumn / UIScrollable / экраны / кнопки / виджеты)

TypeScript
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"

Флекс-ребёнок (все элементы)

TypeScript
flex:        number | string
flexGrow:    number            // по умолчанию 0 — без него ничего не растёт
flexShrink:  number            // по умолчанию 0 — и ничего не сжимается
flexBase:    UIValue | "auto"  // внимание: flexBase, не flexBasis
alignSelf:   "flex-start" | "center" | "flex-end" | "stretch"
aspectRatio: number

Размер и позиция (все элементы)

TypeScript
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"

Отступы и поля (все элементы)

Сокращения и полные формы взаимозаменяемы (ppadding, ptpaddingTop, …):

TypeScript
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).

TypeScript
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

Слои фона рисуются снизу вверх: bgColorbgImagebgGradient — поэтому градиент поверх изображения делает привычный затемняющий scrim для защиты текста:

TypeScript
.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 стопа:

TypeScript
.style({ bgGradient: "linear-gradient(135deg, #FF4032, #7B2FF7)" })
.style({ bgGradient: "linear-gradient(to right, #000 0%, #333 60%, #fff 100%)" })

Радиальный — необязательный префикс [<shape> || <extent>]? [at <position>]?, затем ≥2 стопа:

TypeScript
.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-области:

TypeScript
.style({ bgColor: "#0a0a1a", bgGradient:
  "radial-gradient(circle at 15% 20%, #7B2FF7, transparent 60%), " +
  "radial-gradient(circle at 85% 80%, #2FB0F7, transparent 55%)" })

Линейные и радиальные слои можно смешивать, каждый учитывает полный синтаксис выше, а стек композится на каждой поверхности (браузерный предпросмотр, iOS, headless-рендерер) — обычный способ строить мягкие мульти-свечения или «авроровые» фоны.

Note

bgImage — декор за детьми. Изображение, которое является контентом, — это UIImage — см. content.md.

Всё остальное (все элементы)

TypeScript
opacity:       number | string          // 0..1
transform:     string                   // строка в стиле CSS, напр. `translateY(12px)` — отлично для жестов
display:       "none" | "flex"          // "none" убирает из раскладки
overflow:      "visible" | "hidden"     // по умолчанию "hidden" — дети обрезаются
pointerEvents: "all" | "none"           // рисуемые элементы; "none" пропускает тапы насквозь

Значения и единицы (UIValue)

TypeScript
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 и вкладываются свободно — но не поддерживают %.
TypeScript
width: "calc(100% - 20px)"   // ✗ % не может быть внутри calc
width: "calc(100vw - 20px)"  // ✓ используй единицы вьюпорта

Цвета (только UI-стили)

Строки цвета UI парсятся UI-движком и принимают больше, чем ColorInput со стороны движка (соглашения):

TypeScript
"#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"
Note

этот парсер существует только для UI-стилей. API 2D/3D (Sprite.color, Material, фон Scene2D) принимают только hex-строки и упакованные int — rgba(...) и именованные цвета там молча становятся непрозрачным чёрным.

Безопасные зоны

Ключевые слова, разрешающиеся в безопасные отступы устройства (вырез, home-индикатор). Точный набор:

TypeScript
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() — канонический паттерн для шапки:
TypeScript
.style({ pt: "max(safe-top, 24px)" })   // не меньше 24px даже на устройствах без вырезов

Варианты -comfort никогда не схлопываются в ноль: они гарантируют комфортное пространство для дыхания (≥ 12 логических px по вертикали по умолчанию), даже когда сырой отступ равен 0 — используй их для контента, который иначе касался бы края экрана на старых устройствах.

Адаптивность: onLandscape / onPortait

Любой объект стиля может вкладывать переопределения ориентации; они сливаются сверху, когда устройство в этой ориентации, и снимаются, когда она меняется:

TypeScript
screen.style({ flexDirection: "column", p: 16, onLandscape: { flexDirection: "row", p: 32 } })
Note

ключ портрета действительно пишется onPortait (без второй «r») — это реальное имя API.

Классы стилей

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

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

TypeScript
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")   // → true
TypeScript
el.setClass(name, enabled): this   // активировать (true) / деактивировать (false) класс на этом элементе
el.toggleClass(name): this         // переключить
el.hasClass(name): boolean         // активен ли он сейчас на этом элементе?
  • Имя даётся с ведущим $ или безsetClass("checked", …) и setClass("$checked", …) эквивалентны.
  • Активация на элемент: перестилизует только тот элемент, на котором вызвано, не его детей.
  • Активный набор запоминается на элементе, поэтому класс остаётся активным при закрытии и повторном открытии экрана.
  • Переход (duration / delay, мс) заставляет смену анимироваться, на каждой платформе.

Приоритет — когда два состояния задают одно свойство, побеждает более позднее и более интерактивное, в таком порядке (низший → высший):

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

Так класс бьёт базовый стиль, класс, объявленный позже, бьёт объявленный раньше, а onPressed (или onFocused) всегда бьёт класс, пока элемент нажат/в фокусе. Пример — отмеченная кнопка, которая всё ещё показывает отклик на нажатие:

TypeScript
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 или прокручиваемом экране.

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

TypeScript
display: "grid"                    // ✗ только флексбокс (Yoga)
lineHeight: 1.5                    // ✗ число это px (= 1.5px); множитель это "1.5em"
node.style({ width: "calc(100% - 20px)" })   // ✗ без % внутри calc — используй 100vw

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