LeCodesdocs

Первый экран

Шаг 1 UI-трека. Начинаем проект трека — небольшой трекер привычек — с его главного экрана: заголовок, полоса статистики, список карточек-привычек и кнопка. Состояния и интерактива пока нет (это следующий шаг); сегодня — вёрстка и стили.

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

Скелет

Каждое UI-приложение начинается одинаково: UIScreen, внутри — дети, в конце — .open().

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

TypeScript
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, ни регистрации, учить нечего:

TypeScript
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 на текстовой колонке расталкивает остальное. Здесь она работает сама по себе, списком из трёх:

TypeScript
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 — он съедает всё свободное место по вертикали и прижимает кнопку к низу экрана, сколько бы карточек ни было.

TypeScript
UISpacer(),

UIButton([
  UIText("+ Новая привычка").style({ color: "white", fontSize: 16, fontWeight: 700 }),
]).style({ bgColor: "#FF4032", borderRadius: 16, p: 16, alignItems: "center" }),

Кнопка пока ничего не делает — ровно с этого места продолжит следующий шаг.

Экран целиком

Всё со страницы, собранное сверху донизу, — приложение из вступления:

TypeScript
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; цвета задавай явно.

Справочник по всему, что здесь использовано: Модель элементов · Стилизация · Контейнеры · Текст, изображения, видео · Кнопки · Даты.

Дальше: состояние и реактивность — кнопка начнёт добавлять привычки, а статистика — считать. (пишется)