LeCodesdocs

Your first screen

Step 1 of the UI track. We start the track's project — a small habit tracker — with its home screen: a header, a row of stats, a list of habit cards and a button. No state or interactivity yet (that's the next step); today is layout and styles.

Every example on this page is a complete program: the phone next to you shows exactly the code you're reading, and any example can be pasted into a fresh project as-is.

The skeleton

Every UI app starts the same way: a UIScreen, children inside, .open() at the end.

TypeScript
const screen = UIScreen([
  UIText("Today").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()

What to notice before we build on it:

  • A screen always fills the device — you don't size it, you style its padding, background and how children lay out. pt: "max(safe-top, 24px)" keeps the header clear of the notch on real phones and still gives 24 px in the browser.
  • A screen behaves as a column: children stack top to bottom, gap spaces them.
  • The subtitle isn't a string — it's a call to the SDK's date(): format("dddd, DD MMMM") builds today's date in the device's language, English or Russian. That's why the phone next to you shows the real "today".
  • All sizes are logical px (like CSS px / iOS pt), and .style() merges — a later call only overrides the keys it mentions.
  • The default background is black and text has no default color — always set bgColor and text colors explicitly.

Rows inside columns

The stats strip is the classic flexbox move: a UIRow of three cards, each a small UIColumn. Two defaults matter here and differ from the web: nothing grows or shrinks unless you say so (flexGrow / flexShrink default to 0), and alignItems defaults to stretch.

TypeScript
const screen = UIScreen([
  UIText("Today").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("done").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("left").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("this week").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 on each card is what makes the three of them share the row's width equally. flex: 1 is flexGrow: 1 together with flexBasis: 0: each card starts from zero base width, so they split the row into exact thirds instead of growing from different content-based widths (flexGrow: 1 alone isn't enough — the "71%" card would stay a bit wider). Drop it and the cards hug their text instead. And yes — three copies of the same card is painful to read. That's deliberate: it's the next section's job.

Compose with functions

Three near-identical cards beg for a function — and that's the whole component model of LeCodes UI: elements are plain objects, so a "component" is just a function that returns one. No JSX, no registration, nothing to learn:

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("Today").style({ color: "white", fontSize: 32, fontWeight: 800 }),
  UIText(date().format("dddd, DD MMMM")).style({ color: "#8a8a93", fontSize: 15 }),

  UIRow([ 
    Stat("3", "done"), 
    Stat("2", "left"), 
    Stat("71%", "this week") 
  ]).style({ gap: 10, mt: 8 }),
]).style({ bgColor: "#0e0e12", p: 20, pt: "max(safe-top, 24px)", gap: 4 })

screen.open()

The phone shows the same screen as before — that's the point. Same pixels, a third of the code.

The habit card is the same idea with a bit more layout — a row of [check circle | title+time], where flexGrow: 1 on the text column pushes everything else aside. Here it runs on its own, a list of three:

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("Morning run", "07:30", "#34c759", true),
    Habit("Read 20 pages", "13:00", "#ff9f0a", true),
    Habit("Stretch", "20:30", "#af6bff", false),
  ]).style({ gap: 10 }),
]).style({ bgColor: "#0e0e12", p: 20, pt: "max(safe-top, 24px)" })

screen.open()

The check circle is a fixed 28×28 UIColumn with borderRadius at half its size — there is no special "circle" element, and centering its glyph is the usual alignItems: "center", justifyContent: "center".

Pinning the button to the bottom

The last trick: UISpacer() between the list and the button. A spacer is an empty element with flexGrow: 1 — it eats all the free vertical space, pushing the button to the bottom of the screen no matter how many cards there are.

TypeScript
UISpacer(),

UIButton([
  UIText("+ New habit").style({ color: "white", fontSize: 16, fontWeight: 700 }),
]).style({ bgColor: "#FF4032", borderRadius: 16, p: 16, alignItems: "center" }),

The button doesn't do anything yet — wiring it up is exactly where the next step picks up.

The whole screen

Everything from this page assembled, top to bottom — the app from the intro:

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("Today").style({ color: "white", fontSize: 32, fontWeight: 800 }),
  UIText(date().format("dddd, DD MMMM")).style({ color: MUTED, fontSize: 15 }),

  UIRow([
    Stat("3", "done"),
    Stat("2", "left"),
    Stat("71%", "this week"),
  ]).style({ gap: 10, mt: 8 }),

  UIColumn([
    Habit("Morning run", "07:30", "#34c759", true),
    Habit("Read 20 pages", "13:00", "#ff9f0a", true),
    Habit("Practice piano", "18:00", "#5e9eff", true),
    Habit("Stretch", "20:30", "#af6bff", false),
    Habit("Plan tomorrow", "22:00", ACCENT, false),
  ]).style({ gap: 10, mt: 12 }),

  UISpacer(),

  UIButton([
    UIText("+ New habit").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()

What you learned

  • UIScreen fills the device; style its padding (safe-top / safe-bottom), background and gap — not its size.
  • Layout is flexbox: columns by default, UIRow for rows, flex: 1 to split space equally (flexBasis: 0), flexGrow: 1 to grab the leftover, UISpacer() to push things apart.
  • A component is a plain function returning an element — composition needs no framework.
  • .style() merges; sizes are logical px; set colors explicitly.

Reference for everything used here: Element model · Styling · Containers · Text, images, video · Buttons · Dates.

Next: State & reactivity — the button starts adding habits and the stats start counting. (being written)