Первый экран
Шаг 1 UI-трека. Начинаем проект трека — небольшой трекер привычек — с его главного экрана: заголовок, полоса статистики, список карточек-привычек и кнопка. Состояния и интерактива пока нет (это следующий шаг); сегодня — вёрстка и стили.
Каждый пример на этой странице — законченная программа: телефон рядом показывает ровно тот код, который ты читаешь, и любой пример можно вставить в свежий проект как есть.
Скелет
Каждое UI-приложение начинается одинаково: UIScreen, внутри — дети, в конце — .open().
const screen = UIScreen([
UIText("Сегодня").style({ color: "white", fontSize: 32, fontWeight: 800 }),
UIText(date().format("dddd, DD MMMM")).style({ color: "#8a8a93", fontSize: 15 }),
]).style({ bgColor: "#0e0e12", p: 20, pt: "max(safe-top, 24px)", gap: 4 })
screen.open()Что здесь стоит заметить, прежде чем строить дальше:
- Экран всегда заполняет устройство — ему не задают размер, а стилизуют отступы, фон и раскладку детей.
pt: "max(safe-top, 24px)"уводит заголовок из-под чёлки на реальных телефонах и всё равно даёт 24 px в браузере. - Экран ведёт себя как колонка: дети складываются сверху вниз,
gapих разделяет. - Подзаголовок — не строка, а вызов SDK-функции date():
format("dddd, DD MMMM")собирает сегодняшнюю дату на языке устройства — по-русски или по-английски. Поэтому в телефоне рядом — настоящее «сегодня». - Все размеры — логические px (как CSS px / iOS pt), а
.style()сливает стили: поздний вызов переопределяет только те ключи, которые упоминает. - Фон по умолчанию чёрный, у текста нет цвета по умолчанию — всегда задавай
bgColorи цвета текста явно.
Ряды внутри колонок
Полоса статистики — классический флексбокс-приём: UIRow из трёх карточек, каждая — маленькая UIColumn. Здесь важны два дефолта, отличающихся от веба: ничто не растёт и не сжимается, пока не попросишь (flexGrow / flexShrink по умолчанию 0), а alignItems по умолчанию stretch.
const screen = UIScreen([
UIText("Сегодня").style({ color: "white", fontSize: 32, fontWeight: 800 }),
UIText(date().format("dddd, DD MMMM")).style({ color: "#8a8a93", fontSize: 15 }),
UIRow([
UIColumn([
UIText("3").style({ color: "white", fontSize: 22, fontWeight: 800 }),
UIText("готово").style({ color: "#8a8a93", fontSize: 13 }),
]).style({ bgColor: "#1a1a20", borderRadius: 14, p: 12, gap: 2, flex: 1, alignItems: "center" }),
UIColumn([
UIText("2").style({ color: "white", fontSize: 22, fontWeight: 800 }),
UIText("осталось").style({ color: "#8a8a93", fontSize: 13 }),
]).style({ bgColor: "#1a1a20", borderRadius: 14, p: 12, gap: 2, flex: 1, alignItems: "center" }),
UIColumn([
UIText("71%").style({ color: "white", fontSize: 22, fontWeight: 800 }),
UIText("за неделю").style({ color: "#8a8a93", fontSize: 13 }),
]).style({ bgColor: "#1a1a20", borderRadius: 14, p: 12, gap: 2, flex: 1, alignItems: "center" }),
]).style({ gap: 10, mt: 8 }),
]).style({ bgColor: "#0e0e12", p: 20, pt: "max(safe-top, 24px)", gap: 4 })
screen.open()Именно flex: 1 на каждой карточке заставляет три штуки поровну делить ширину ряда. flex: 1 — это flexGrow: 1 вместе с flexBasis: 0: карточки стартуют с нулевой базовой ширины, поэтому делят строку ровно натрое, а не растут от разной по содержимому базы (одного flexGrow: 1 для этого мало — карточка с «71%» осталась бы чуть шире). Убери его — и карточки обожмутся вокруг своего текста. И да — три копии одной карточки читать больно. Это нарочно: ими займётся следующий раздел.
Композиция функциями
Три почти одинаковые карточки просятся в функцию — и это вся компонентная модель LeCodes UI: элементы — обычные объекты, поэтому «компонент» — просто функция, которая возвращает элемент. Ни JSX, ни регистрации, учить нечего:
const Stat = (value: string, label: string) =>
UIColumn([
UIText(value).style({ color: "white", fontSize: 22, fontWeight: 800 }),
UIText(label).style({ color: "#8a8a93", fontSize: 13 }),
]).style({ bgColor: "#1a1a20", borderRadius: 14, p: 12, gap: 2, flex: 1, alignItems: "center" })
const screen = UIScreen([
UIText("Сегодня").style({ color: "white", fontSize: 32, fontWeight: 800 }),
UIText(date().format("dddd, DD MMMM")).style({ color: "#8a8a93", fontSize: 15 }),
UIRow([
Stat("3", "готово"),
Stat("2", "осталось"),
Stat("71%", "за неделю")
]).style({ gap: 10, mt: 8 }),
]).style({ bgColor: "#0e0e12", p: 20, pt: "max(safe-top, 24px)", gap: 4 })
screen.open()В телефоне — тот же экран, что и раньше. В этом и смысл: те же пиксели, треть кода.
Карточка привычки — та же идея, но с чуть большей раскладкой: ряд из [кружок-галочка | заголовок+время], где flexGrow: 1 на текстовой колонке расталкивает остальное. Здесь она работает сама по себе, списком из трёх:
const Habit = (title: string, time: string, color: string, done: boolean) =>
UIRow([
UIColumn([
UIText(done ? "✓" : "").style({ color: "white", fontSize: 15, fontWeight: 700 }),
]).style({
width: 28, height: 28, borderRadius: 14,
bgColor: done ? color : "transparent",
border: done ? "0" : `2px solid ${color}`,
alignItems: "center", justifyContent: "center",
}),
UIColumn([
UIText(title).style({ color: "white", fontSize: 16, fontWeight: 600 }),
UIText(time).style({ color: "#8a8a93", fontSize: 13 }),
]).style({ gap: 2, flexGrow: 1 }),
]).style({ bgColor: "#1a1a20", borderRadius: 16, p: 14, gap: 12, alignItems: "center", opacity: done ? 0.75 : 1 })
const screen = UIScreen([
UIColumn([
Habit("Утренняя пробежка", "07:30", "#34c759", true),
Habit("Прочитать 20 страниц", "13:00", "#ff9f0a", true),
Habit("Растяжка", "20:30", "#af6bff", false),
]).style({ gap: 10 }),
]).style({ bgColor: "#0e0e12", p: 20, pt: "max(safe-top, 24px)" })
screen.open()Кружок с галочкой — фиксированная UIColumn 28×28 с borderRadius в половину размера: отдельного элемента «круг» нет, а центрирование значка — обычные alignItems: "center", justifyContent: "center".
Прижать кнопку к низу
Последний приём: UISpacer() между списком и кнопкой. Спейсер — пустой элемент с flexGrow: 1 — он съедает всё свободное место по вертикали и прижимает кнопку к низу экрана, сколько бы карточек ни было.
UISpacer(),
UIButton([
UIText("+ Новая привычка").style({ color: "white", fontSize: 16, fontWeight: 700 }),
]).style({ bgColor: "#FF4032", borderRadius: 16, p: 16, alignItems: "center" }),Кнопка пока ничего не делает — ровно с этого места продолжит следующий шаг.
Экран целиком
Всё со страницы, собранное сверху донизу, — приложение из вступления:
const ACCENT = "#FF4032"
const BG = "#0e0e12"
const CARD = "#1a1a20"
const MUTED = "#8a8a93"
const Stat = (value: string, label: string) =>
UIColumn([
UIText(value).style({ color: "white", fontSize: 22, fontWeight: 800 }),
UIText(label).style({ color: MUTED, fontSize: 13 }),
]).style({ bgColor: CARD, borderRadius: 14, p: 12, gap: 2, flex: 1, alignItems: "center" })
const Habit = (title: string, time: string, color: string, done: boolean) =>
UIRow([
UIColumn([
UIText(done ? "✓" : "").style({ color: "white", fontSize: 15, fontWeight: 700 }),
]).style({
width: 28, height: 28, borderRadius: 14,
bgColor: done ? color : "transparent",
border: done ? "0" : `2px solid ${color}`,
alignItems: "center", justifyContent: "center",
}),
UIColumn([
UIText(title).style({ color: "white", fontSize: 16, fontWeight: 600 }),
UIText(time).style({ color: MUTED, fontSize: 13 }),
]).style({ gap: 2, flexGrow: 1 }),
]).style({ bgColor: CARD, borderRadius: 16, p: 14, gap: 12, alignItems: "center", opacity: done ? 0.75 : 1 })
const screen = UIScreen([
UIText("Сегодня").style({ color: "white", fontSize: 32, fontWeight: 800 }),
UIText(date().format("dddd, DD MMMM")).style({ color: MUTED, fontSize: 15 }),
UIRow([
Stat("3", "готово"),
Stat("2", "осталось"),
Stat("71%", "за неделю"),
]).style({ gap: 10, mt: 8 }),
UIColumn([
Habit("Утренняя пробежка", "07:30", "#34c759", true),
Habit("Прочитать 20 страниц", "13:00", "#ff9f0a", true),
Habit("Позаниматься на пианино", "18:00", "#5e9eff", true),
Habit("Растяжка", "20:30", "#af6bff", false),
Habit("Спланировать завтра", "22:00", ACCENT, false),
]).style({ gap: 10, mt: 12 }),
UISpacer(),
UIButton([
UIText("+ Новая привычка").style({ color: "white", fontSize: 16, fontWeight: 700 }),
]).style({ bgColor: ACCENT, borderRadius: 16, p: 16, alignItems: "center" }),
]).style({ bgColor: BG, p: 20, pt: "max(safe-top, 24px)", pb: "max(safe-bottom, 20px)", gap: 4 })
screen.open()Что ты узнал
UIScreenзаполняет устройство; стилизуй его отступы (safe-top/safe-bottom), фон иgap— но не размер.- Раскладка — флексбокс: по умолчанию колонки,
UIRowдля рядов,flex: 1— поделить место поровну (flexBasis: 0),flexGrow: 1— забрать остаток,UISpacer()— расталкивать. - Компонент — обычная функция, возвращающая элемент; для композиции не нужен фреймворк.
.style()сливает стили; размеры — логические px; цвета задавай явно.
Справочник по всему, что здесь использовано: Модель элементов · Стилизация · Контейнеры · Текст, изображения, видео · Кнопки · Даты.
Дальше: состояние и реактивность — кнопка начнёт добавлять привычки, а статистика — считать. (пишется)