LeCodesdocs

UIWidget

A floating overlay that lives outside the screen system: always position-fixed relative to the device, drawn above the active screen, and untouched by Router navigation. Use it for bottom sheets, dialogs, toasts-with-actions, floating players. Create a widget once at module scope and reuse it — visibility is imperative.

At a glance

TypeScript
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())

// anywhere, on any screen:
dialog.show()

Creating & visibility

TypeScript
UIWidget(): UIWidget
UIWidget(children: UINodeChild[]): UIWidget

widget.show(): void        // mount and display (hidden by default)
widget.hide(): void        // unmount
widget.isShow: boolean     // getter — currently shown?

Full container surface: .append / .insert / .remove / .setContent, .style, .onLayout. Position it with top / left / right / bottom / width / height — the coordinates are device-screen space (safe-area values like "safe-bottom" work).

Note

a widget persists across Router.push / pop. If it belongs to one screen only, pair it with the screen's lifecycle: .onOpen(() => sheet.show()), .onClose(() => sheet.hide()).

TypeScript
widget.style({ overlayColor: "rgba(0, 0, 0, 0.5)" })   // Color | null
widget.onOverlayTap(cb: () => void): this

overlayColor adds a full-screen scrim behind the widget that blocks all taps underneath — this is what turns a widget into a modal (dialog / bottom sheet). "transparent" is invisible but still intercepts; null (the default) removes the layer entirely. A tap on the scrim fires onOverlayTap — usually () => widget.hide().

Touch & back button

TypeScript
widget.onTouchStart(ev => …)           // ev.track({...}) for drag gestures (sheet dragging)
widget.onBackPressed(cb: () => void)   // Android back while the widget is up — usually hide()

Widgets are one of the three touch-receiving elements (with UIButton and UIScreen) — see Pointer events.

Exit animations — animateTo with commit: false

TypeScript
widget.animateTo({ overlayColor, opacity, transform, …, duration?, delay?, commit?: boolean })
widget.animateFrom({ … })              // animate from the given values to the current style

animateTo normally writes the target values into the widget's style when it starts. For an exit animation that's wrong — the next show() would start from the faded-out values. Pass commit: false to play the animation without persisting it, and hide() when done:

TypeScript
const close = () => {
  widget.animateTo({ opacity: 0, commit: false, duration: 250 })
  setTimeout(() => widget.hide(), 250)   // style still has opacity 1 for the next show()
}

Bottom sheet — the canonical widget

Drag-to-dismiss sheet: position driven by transform, scrim via overlayColor, drag captured with claim: "pan-y".

TypeScript
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())                              // slide out, then unmount
  sheet.animateTo({ overlayColor: "transparent", commit: false })  // fade the scrim, don't persist
}

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",              // win the vertical gesture from any scroller inside
}))
.onOverlayTap(close)
.onBackPressed(close)
.onLayout(l => { height = l.height })

Pitfalls

TypeScript
// ✗ re-creating the widget per screen / per show
const openSheet = () => UIWidget([...]).show()     // leaks a new widget every call
// ✓ create once at module scope, show()/hide() the same instance

// ✗ expecting it to appear on creation
const w = UIWidget([...])                          // hidden until w.show()

See also