UIWidget
Плавающий оверлей, живущий вне системы экранов: всегда position-fixed относительно устройства,
рисуется над активным экраном и не тронут навигацией Router. Используй для нижних шторок, диалогов,
тостов-с-действиями, плавающих плееров. Создай виджет один раз на уровне модуля и переиспользуй —
видимость императивна.
Обзор
const dialog = UIWidget([
UIText("Delete item?").style({ fontWeight: 700, fontSize: 18, color: "black" }),
UIButton([ UIText("Delete").style({ color: "white" }) ])
.style({ bgColor: "#FF4032", borderRadius: 8, height: 40, justifyContent: "center" })
.onClick(() => { doDelete(); dialog.hide() }),
])
.style({ left: 24, right: 24, top: "40%", p: 16, gap: 12, borderRadius: 16,
bgColor: "white", overlayColor: "rgba(0,0,0,0.5)" })
.onOverlayTap(() => dialog.hide())
.onBackPressed(() => dialog.hide())
// где угодно, на любом экране:
dialog.show()Создание и видимость
UIWidget(): UIWidget
UIWidget(children: UINodeChild[]): UIWidget
widget.show(): void // смонтировать и показать (по умолчанию скрыт)
widget.hide(): void // размонтировать
widget.isShow: boolean // геттер — показан ли сейчас?Полная поверхность контейнера: .append / .insert / .remove / .setContent, .style,
.onLayout. Позиционируй его через top / left / right / bottom / width / height —
координаты в пространстве экрана устройства (значения safe-area вроде "safe-bottom" работают).
виджет сохраняется через Router.push / pop. Если он принадлежит одному экрану, спаруй его с
жизненным циклом экрана: .onOpen(() => sheet.show()), .onClose(() => sheet.hide()).
Модальное поведение — overlayColor
widget.style({ overlayColor: "rgba(0, 0, 0, 0.5)" }) // Color | null
widget.onOverlayTap(cb: () => void): thisoverlayColor добавляет полноэкранный scrim за виджетом, блокирующий все тапы под ним — это и
превращает виджет в модалку (диалог / нижнюю шторку). "transparent" невидим, но всё равно
перехватывает; null (по умолчанию) убирает слой целиком. Тап по scrim'у вызывает onOverlayTap —
обычно () => widget.hide().
Касания и кнопка «назад»
widget.onTouchStart(ev => …) // ev.track({...}) для жестов перетаскивания (перетаскивание шторки)
widget.onBackPressed(cb: () => void) // Android «назад», пока виджет поднят — обычно hide()Виджеты — один из трёх элементов, принимающих касания (с UIButton и UIScreen) — см.
События указателя.
Анимации выхода — animateTo с commit: false
widget.animateTo({ overlayColor, opacity, transform, …, duration?, delay?, commit?: boolean })
widget.animateFrom({ … }) // анимировать от заданных значений к текущему стилюanimateTo обычно пишет целевые значения в стиль виджета при старте. Для анимации выхода это
неверно — следующий show() стартовал бы с затухших значений. Передай commit: false, чтобы
проиграть анимацию без сохранения, и hide() по завершении:
const close = () => {
widget.animateTo({ opacity: 0, commit: false, duration: 250 })
setTimeout(() => widget.hide(), 250) // стиль всё ещё с opacity 1 для следующего show()
}Нижняя шторка — канонический виджет
Шторка drag-to-dismiss: позиция управляется transform, scrim через overlayColor, перетаскивание
захвачено claim: "pan-y".
let pos = 0, height = 0
const slideTo = (to: number, done?: () => void) => animate({
from: pos, to, duration: 300, easing: cubicBezier(0.2, 0, 0.2, 1),
onUpdate(v) { pos = v; sheet.style.transform = `translateY(${v}px)` },
onComplete() { done?.() },
})
const close = () => {
slideTo(height, () => sheet.hide()) // выехать, затем размонтировать
sheet.animateTo({ overlayColor: "transparent", commit: false }) // затухание scrim'а, не сохранять
}
const sheet = UIWidget([
UIColumn([]).style({ width: 50, height: 6, borderRadius: 3, bgColor: "#D9D9D9", mx: "auto", my: 12 }),
UIText("Title").style({ px: 16, fontWeight: 700, fontSize: 20, color: "black" }),
])
.style({ bgColor: "white", borderRadius: 20, bottom: 0, left: 0, right: 0, height: 400,
overlayColor: "rgba(0, 0, 0, 0.5)" })
.onTouchStart(ev => ev.track({
onMove({ deltaY }) { pos = Math.max(0, pos + deltaY); sheet.style.transform = `translateY(${pos}px)` },
onEnd() { pos > height * 0.3 ? close() : slideTo(0) },
claim: "pan-y", // выиграть вертикальный жест у любого скроллера внутри
}))
.onOverlayTap(close)
.onBackPressed(close)
.onLayout(l => { height = l.height })Подводные камни
// ✗ пересоздание виджета на каждый экран / на каждый показ
const openSheet = () => UIWidget([...]).show() // течёт новый виджет на каждый вызов
// ✓ создай один раз на уровне модуля, show()/hide() тот же экземпляр
// ✗ ждать, что он появится при создании
const w = UIWidget([...]) // скрыт до w.show()Смотрите также
- UIScreen и Router — стек экранов, над которым плавают виджеты.
- События указателя и жесты —
ev.track(),claim. - Стилизация —
animate,animateTo, трансформы.