LeCodesdocs

UIPager

UIPager — единственный элемент навигации по экранам: вкладки-сиблинги, нативно свайпающиеся в стороны, каждая со своим стеком навигации. Передай массив экранов — они встанут рядом друг с другом; push открывает экран поверх текущей вкладки (внутри пейджера), а нативный краевой свайп назад (или pop) его разматывает. Он мапится на нативные контейнеры каждой платформы (iOS UIPageViewController + UINavigationController на вкладку), так что свайпы, пуши и жест «назад» — настоящие, а не переизобретённые.

TypeScript
const pager = UIPager(HomeScreen(), SearchScreen(), ProfileScreen())

// позже, откуда угодно внутри хостимого экрана — без проброса ссылки:
UIPager.push(PostScreen(id))   // въезжает поверх текущей вкладки
UIPager.pop()

Пейджер с одной вкладкой — это просто стек навигации: отдельного элемента-стека нет. Хосты понимают это буквально: с одной вкладкой iOS вовсе пропускает пейджинговый контейнер и встраивает только UINavigationController, так что накладных расходов против выделенного компонента-стека — ноль.

UITabs — стандартная оболочка с нижними вкладками

Для типовой формы — пейджер + нижняя панель — используй UITabs вместо ручной сборки. Это чистая SDK-композиция (UIScreen из [ UIPager (flexGrow: 1), темированная панель ], ноль нового ABI) со встроенными синхронизацией выбора, подсветкой активной вкладки, отступом под безопасную зону и бейджами. Ключи — id вкладок, в порядке вкладок; кнопки называются tab-<id> (flow-тесты могут тапать их по имени):

TypeScript
const tabs = UITabs({
  home:    { label: "Home",    icon: assetIcon("lucide:house"), screen: homeScreen },
  profile: { label: "Profile", icon: assetIcon("lucide:user"),  screen: profileScreen },
})
Router.init(tabs)                 // UITabs И ЕСТЬ UIScreen / Presentable

tabs.select("profile")            // переключить вкладку (мгновенно; `true` — слайд)
tabs.tab                          // id активной вкладки (геттер)
tabs.onSelect((id, i) => ...)     // тап по панели, свайп или select()
tabs.badge("home", 3)             // пилюля со счётчиком; `true` = точка; false/null/0 очищает
tabs.pager                        // пейджер под панелью (push/pop/depth)

Панель стилизует себя через переменные темы, с фолбэками, чтобы нетемированное приложение всё равно выглядело правильно: активное var(--primaryColor), неактивное var(--mutedColor), поверхность var(--tabbarBg), волосяная линия var(--tabbarBorder), бейдж var(--badgeColor) — один вызов theme({ primaryColor, tabbarBg }) перестилизует её на всё приложение (те же переменные читает панель defineTabs дизайн-скаффолда). Активная кнопка также несёт класс стиля active (каскадирует на её контент). Для более тонкого контроля перестилизуй tabs.bar; для полностью кастомной панели спустись к UIPager + собственной строке.

Навигация внутри вкладки не меняется: UIPager.push(detail) с любого экрана сохраняет панель; Router.push(...) открывается над всей оболочкой (панель закрыта).

Конструирование

TypeScript
UIPager(root)                          // одна вкладка = простой стек с корнем `root`
UIPager(a, b, c)                       // три вкладки рядом (выбрана a)
// Вкладки вариадичны, как дети контейнера: аргумент-массив расплющивается, falsy-элементы
// пропускаются — UIPager(home, isAdmin && adminTab) работает. Стили — через чейнящийся .style({...}).

Дети — только UIScreenы — каждая страница несёт полный контракт экрана (onOpen/onClose, onBackPressed) и раскладывается в бокс пейджера. Чтобы хостить простой элемент, оберни его: UIPager(UIScreen(el)). Вкладки фиксируются при конструировании — динамические части — это выбор вкладки и стек каждой из них.

Пейджер заполняет свой слот как любой элемент — дай ему flexGrow: 1 (или размер), чтобы он не схлопнулся (flexShrink: 1 уже по умолчанию, как у UIScrollable, так что он оставляет место таб-бару):

TypeScript
UIScreen(
  UIPager(HomeTab(), ProfileTab()).style({ flexGrow: 1 }),
  tabBar,
)

Вкладки

TypeScript
pager.select(2)                  // мгновенное переключение (no-op вне диапазона)
pager.select(2, true)            // анимированный слайд с учётом направления
pager.index                      // выбранная вкладка (геттер)
pager.onSelect(i => { ... })     // срабатывает на устоявшийся свайп или select()

select мгновенен по умолчанию — платформенная конвенция для таб-баров, где вкладки — параллельные режимы, а не последовательность слева направо. Передай animated: true для скользящего переключения top-tab-интерфейсов (ощущение Material). Свайп всегда анимирован — это физическое перетаскивание.

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

Свайп между вкладками работает только на корне вкладки. Как только текущая вкладка углублена (depth > 1), горизонтальный жест принадлежит краевому свайпу назад; пейджинг снова включается, когда вкладка вернулась к корню. Это нативный арбитраж — настраивать нечего.

Стек (на вкладку)

TypeScript
pager.push(screen)     // въезжает новой вершиной на ТЕКУЩУЮ вкладку; свайп назад её снимает
pager.pop()            // снять вершину текущей вкладки (no-op на корне вкладки)
pager.popToRoot()      // размотать текущую вкладку до корня
pager.replace(screen)  // подменить вершину текущей вкладки (на глубине 1 подменяет корень вкладки)

pager.stack            // UIScreen[] текущей вкладки, корень → вершина (геттер)
pager.depth            // 1 = только корень (геттер)
pager.onChange(depth => { ... })   // срабатывает на push, pop, replace или свайп назад

Опции перехода на каждый push нет — это фиксированный нативный push/pop, ровно то, что позволяет жесту «назад» обратимо тянуть его.

Доступ откуда угодно — UIPager.current

UIPager — глобал, так что ссылку в дочерние экраны не пробрасываешь. Амбиентные вызовы действуют на пейджер, владеющий видимым сейчас экраном:

TypeScript
UIPager.current        // пейджер, чей экран видим, или null
UIPager.push(screen)   // → current.push(screen)
UIPager.pop()          // → current.pop()
UIPager.popToRoot()
TypeScript
const HomeTab = () => UIScreen(
  row("Open the first post", () => UIPager.push(PostScreen(posts[0]))),
)

С пейджерами, вложенными в страницы, UIPager.current разрешается во внутреннейшийpush изнутри страницы попадает в пейджер, на который пользователь реально смотрит. Если у видимого экрана нет объемлющего пейджера, currentnull, и амбиентные вызовы — no-op с предупреждением.

Таб-бары

UITabs даёт стандартную нижнюю панель. Для кастомной панели поверх сырого пейджера — собери свою и веди её в обе стороны:

TypeScript
const pager = UIPager(HomeTab(), ProfileTab())
const tabBar = UIRow(
  UIButton(UIText("Home")).onClick(() => pager.select(0)),
  UIButton(UIText("Profile")).onClick(() => pager.select(1)),
)
pager.onSelect(i => highlight(tabBar, i))   // свайпы тоже двигают подсветку

Router.init(UIScreen(pager.style({ flexGrow: 1 }), tabBar))

Кнопка «назад» (Android) и свайп назад

Интерактивный краевой свайп назад снимает верх текущей вкладки автоматически. Для аппаратной/программной кнопки «назад» сначала сними верх текущей вкладки, затем провались в собственную обработку:

TypeScript
screen.onBackPressed(() => {
  if (UIPager.current && UIPager.current.depth > 1) {
    UIPager.current.pop()
    return
  }
  // ...твоё собственное поведение «назад» (выход, подтверждение и т.д.)
})

Связь с Router

Router навигирует между цельноэкранными пунктами назначения и кросс-типовыми переходами (экран ↔ сцена ↔ нативная вью ↔ видео) на корне приложения. UIPager — навигационный регион внутри экрана. Они сосуществуют, и выбор из любой области — это просто глагол: UIPager.push открывает внутри региона (панели остаются), Router.push открывает поверх всего (накрывает весь экран, в котором живёт пейджер). См. screen-router.md. UIPager сам не пункт назначения — ты не вызываешь на нём open() и не передаёшь его в Router.push; встраивай его в экран.