lecodes-cli 0.7.2 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/dist/index.js +2909 -725
  2. package/package.json +2 -2
  3. package/runtime/scene-harness.json +1 -1
  4. package/runtime/sdk/canvas/Canvas.ts +16 -0
  5. package/runtime/sdk/gl/Material.ts +4 -0
  6. package/runtime/sdk/inject.ts +7 -4
  7. package/runtime/sdk/plugins/service.ts +47 -0
  8. package/runtime/sdk/runtime/app.ts +21 -1
  9. package/runtime/sdk/runtime/appEvents.ts +25 -0
  10. package/runtime/sdk/runtime/service.ts +3 -2
  11. package/runtime/sdk/ui/NativeView.ts +2 -1
  12. package/runtime/sdk/ui/UI.ts +3 -1
  13. package/runtime/sdk/ui/UIButton.ts +10 -5
  14. package/runtime/sdk/ui/UIContainer.ts +16 -9
  15. package/runtime/sdk/ui/UIImage.ts +5 -2
  16. package/runtime/sdk/ui/UIInput.ts +75 -8
  17. package/runtime/sdk/ui/UIModal.ts +149 -0
  18. package/runtime/sdk/ui/UINode.ts +113 -50
  19. package/runtime/sdk/ui/UIPager.ts +297 -0
  20. package/runtime/sdk/ui/UIPopover.ts +106 -0
  21. package/runtime/sdk/ui/UIScreen.ts +11 -10
  22. package/runtime/sdk/ui/UIScrollable.ts +2 -1
  23. package/runtime/sdk/ui/UISpacer.ts +1 -1
  24. package/runtime/sdk/ui/UIText.ts +2 -1
  25. package/runtime/sdk/ui/UIVideo.ts +2 -1
  26. package/runtime/sdk/ui/UIVirtualizedList.ts +2 -1
  27. package/runtime/sdk/ui/UIWidget.ts +19 -22
  28. package/runtime/sdk/ui/theme.ts +66 -0
  29. package/runtime/sdk-types.json +1 -1
  30. package/runtime/sdk/compile/aspectMacro.ts +0 -42
  31. package/runtime/sdk/compile/assetMacro.ts +0 -45
  32. package/runtime/sdk/compile/bundler.ts +0 -207
  33. package/runtime/sdk/compile/compileProject.ts +0 -113
  34. package/runtime/sdk/compile/header.ts +0 -46
  35. package/runtime/sdk/compile/index.ts +0 -18
  36. package/runtime/sdk/compile/libraryImports.ts +0 -47
  37. package/runtime/sdk/compile/sceneEditor.ts +0 -78
  38. package/runtime/sdk/compile/sourcemap.ts +0 -25
  39. package/runtime/sdk/kit/UITabs.ts +0 -105
  40. package/runtime/sdk/ui/UIScreenHost.ts +0 -213
@@ -1,6 +1,6 @@
1
1
  import { ClickEvent, TouchStartEvent, LongPressEvent } from "../runtime/touch"
2
2
  import type { ClickEvent as ClickEventType, TouchStartEvent as TouchStartEventType, LongPressEvent as LongPressEventType } from "../runtime/touch"
3
- import { type ElementStyle, type ContainerStyle, type Style, ContainerElement, type DrawableStyle, type UINodeChild, type ChildrenFn, type OnLayoutCallback, type BaseStyle, type AnimateStyle, type Color } from "./UINode"
3
+ import { type ElementStyle, type ContainerStyle, type Style, ContainerElement, type DrawableStyle, type UINodeChild, type ChildrenFn, type OnLayoutCallback, type BoundingClientRect, type BaseStyle, type AnimateStyle, type Color } from "./UINode"
4
4
 
5
5
  export type UIButtonStyle = ElementStyle & DrawableStyle & ContainerStyle & { rippleColor?: Color | "default" }
6
6
 
@@ -25,6 +25,7 @@ export interface UIButton {
25
25
  setContent(nodes: UINodeChild[] | ChildrenFn): this
26
26
 
27
27
  onLayout(onLayout: OnLayoutCallback): this
28
+ getBoundingClientRect(): BoundingClientRect | null
28
29
 
29
30
  setClass(name: string, enabled: boolean): this
30
31
  toggleClass(name: string): this
@@ -94,20 +95,24 @@ class ButtonElement extends ContainerElement<"button"> {
94
95
  }
95
96
  }
96
97
 
98
+ // Buttons are row + centered by default: the icon+label button is the norm, and yoga's column
99
+ // default was a recurring authoring/codegen trap.
100
+ const BUTTON_DEFAULTS = { flexDirection: "row", justifyContent: "center", alignItems: "center" } as const
101
+
97
102
  export function UIButton(): UIButton;
98
103
  export function UIButton(children: UINodeChild[] | ChildrenFn): UIButton;
99
104
  export function UIButton(style: UIButtonStyle): UIButton;
100
105
  export function UIButton(style: UIButtonStyle, children: UINodeChild[]): UIButton;
101
106
  export function UIButton(...args: [] | [UINodeChild[] | ChildrenFn | UIButtonStyle] | [UIButtonStyle, children: UINodeChild[] | ChildrenFn]): UIButton {
102
107
  if (args.length === 0) {
103
- return new ButtonElement("button", {}, [])
108
+ return new ButtonElement("button", { ...BUTTON_DEFAULTS }, [])
104
109
  } else if (args.length === 1) {
105
110
  if (Array.isArray(args[0]) || typeof args[0] === "function") {
106
- return new ButtonElement("button", {}, args[0])
111
+ return new ButtonElement("button", { ...BUTTON_DEFAULTS }, args[0])
107
112
  } else {
108
- return new ButtonElement("button", args[0], [])
113
+ return new ButtonElement("button", { ...BUTTON_DEFAULTS, ...args[0] }, [])
109
114
  }
110
115
  } else {
111
- return new ButtonElement("button", args[0], args[1])
116
+ return new ButtonElement("button", { ...BUTTON_DEFAULTS, ...args[0] }, args[1])
112
117
  }
113
118
  }
@@ -1,4 +1,4 @@
1
- import { ContainerElement, type AnimateStyle, type AppearStyle, type ChildrenFn, type ContainerStyle, type DisappearStyle, type DrawableStyle, type ElementStyle, type OnLayoutCallback, type Style, type StyleFn, type UINode, type UINodeChild } from "./UINode"
1
+ import { ContainerElement, type AnimateStyle, type AppearStyle, type ChildrenFn, type ContainerStyle, type DisappearStyle, type DrawableStyle, type ElementStyle, type OnLayoutCallback, type BoundingClientRect, type Style, type StyleFn, type UINode, type UINodeChild } from "./UINode"
2
2
 
3
3
  export type UIContainerStyle = ElementStyle & DrawableStyle & ContainerStyle
4
4
 
@@ -14,6 +14,7 @@ export interface UIColumn {
14
14
  setContent(nodes: UINodeChild[] | ChildrenFn): this
15
15
 
16
16
  onLayout(onLayout: OnLayoutCallback): this
17
+ getBoundingClientRect(): BoundingClientRect | null
17
18
 
18
19
  setClass(name: string, enabled: boolean): this
19
20
  toggleClass(name: string): this
@@ -52,6 +53,7 @@ export interface UIRow {
52
53
  setContent(nodes: UINodeChild[] | ChildrenFn): this
53
54
 
54
55
  onLayout(onLayout: OnLayoutCallback): this
56
+ getBoundingClientRect(): BoundingClientRect | null
55
57
 
56
58
  setClass(name: string, enabled: boolean): this
57
59
  toggleClass(name: string): this
@@ -60,20 +62,21 @@ export interface UIRow {
60
62
 
61
63
  readonly children: UINodeChild[],
62
64
  }
65
+ export function UIRow(): UIRow
63
66
  export function UIRow(children: UINodeChild[] | ChildrenFn): UIRow
64
67
  export function UIRow(style: UIContainerStyle): UIRow
65
68
  export function UIRow(style: UIContainerStyle, children: UINodeChild[]): UIRow
66
69
  export function UIRow(...args: [] | [UINodeChild[] | ChildrenFn | UIContainerStyle] | [UIContainerStyle, children: UINodeChild[] | ChildrenFn]): UIRow {
67
70
  if (args.length === 0) {
68
- return new ContainerElement("row", {}, [])
71
+ return new ContainerElement("row", { flexDirection: "row" }, [])
69
72
  } else if (args.length === 1) {
70
73
  if (Array.isArray(args[0]) || typeof args[0] === "function") {
71
- return new ContainerElement("row", {}, args[0])
74
+ return new ContainerElement("row", { flexDirection: "row" }, args[0])
72
75
  } else {
73
- return new ContainerElement("row", args[0], [])
76
+ return new ContainerElement("row", { flexDirection: "row", ...args[0] }, [])
74
77
  }
75
78
  } else {
76
- return new ContainerElement("row", args[0], args[1])
79
+ return new ContainerElement("row", { flexDirection: "row", ...args[0] }, args[1])
77
80
  }
78
81
  }
79
82
 
@@ -89,6 +92,7 @@ export interface UIBox {
89
92
  setContent(nodes: UINodeChild[] | ChildrenFn): this
90
93
 
91
94
  onLayout(onLayout: OnLayoutCallback): this
95
+ getBoundingClientRect(): BoundingClientRect | null
92
96
 
93
97
  setClass(name: string, enabled: boolean): this
94
98
  toggleClass(name: string): this
@@ -97,19 +101,22 @@ export interface UIBox {
97
101
 
98
102
  readonly children: UINodeChild[],
99
103
  }
104
+ const BOX_DEFAULTS = { justifyContent: "center", alignItems: "center" } as const
105
+
106
+ export function UIBox(): UIBox
100
107
  export function UIBox(children: UINodeChild[] | ChildrenFn): UIBox
101
108
  export function UIBox(style: UIContainerStyle): UIBox
102
109
  export function UIBox(style: UIContainerStyle, children: UINodeChild[]): UIBox
103
110
  export function UIBox(...args: [] | [UINodeChild[] | ChildrenFn | UIContainerStyle] | [UIContainerStyle, children: UINodeChild[] | ChildrenFn]): UIBox {
104
111
  if (args.length === 0) {
105
- return new ContainerElement("box", {}, [])
112
+ return new ContainerElement("box", { ...BOX_DEFAULTS }, [])
106
113
  } else if (args.length === 1) {
107
114
  if (Array.isArray(args[0]) || typeof args[0] === "function") {
108
- return new ContainerElement("box", {}, args[0])
115
+ return new ContainerElement("box", { ...BOX_DEFAULTS }, args[0])
109
116
  } else {
110
- return new ContainerElement("box", args[0], [])
117
+ return new ContainerElement("box", { ...BOX_DEFAULTS, ...args[0] }, [])
111
118
  }
112
119
  } else {
113
- return new ContainerElement("box", args[0], args[1])
120
+ return new ContainerElement("box", { ...BOX_DEFAULTS, ...args[0] }, args[1])
114
121
  }
115
122
  }
@@ -2,7 +2,7 @@ import type { FetchResponse } from "../runtime/fetch"
2
2
  import type { SvgSourceValue as SvgSource } from "../runtime/misc"
3
3
  import type { File } from "../runtime/fetch"
4
4
  import { Canvas, isCanvas } from "../canvas/Canvas"
5
- import { Element, type AnimateStyle, type Color, type ElementStyle, type OnLayoutCallback, type Style } from "./UINode"
5
+ import { Element, type AnimateStyle, type Color, type ElementStyle, type OnLayoutCallback, type BoundingClientRect, type Style } from "./UINode"
6
6
 
7
7
  export type UIImageStyle = ElementStyle & { borderRadius?: number, objectFit?: "cover" | "contain" | "fill", tintColor?: Color }
8
8
 
@@ -14,7 +14,9 @@ export type CanvasImageSource = { canvasSurface: number }
14
14
  export type ImageSource = string | FetchResponse | File | SvgSource | Canvas
15
15
 
16
16
  const toSrc = (src: ImageSource): string | FetchResponse | File | SvgSource | CanvasImageSource =>
17
- isCanvas(src) ? { canvasSurface: src._bakeSurface() } : src
17
+ // _bakeSurfaceForUI (not _bakeSurface) also flags the canvas UI-bound, so canvas.update() later
18
+ // refreshes this image node in place — that's what makes an animated UIImage(canvas) update.
19
+ isCanvas(src) ? { canvasSurface: src._bakeSurfaceForUI() } : src
18
20
 
19
21
  /** The stored/resolved image source (a Canvas is resolved to a surface marker). */
20
22
  export type ResolvedImageSource = string | FetchResponse | File | SvgSource | CanvasImageSource
@@ -35,6 +37,7 @@ export interface UIImage {
35
37
  setSourceRect(x: number, y: number, w: number, h: number): this
36
38
 
37
39
  onLayout(onLayout: OnLayoutCallback): this
40
+ getBoundingClientRect(): BoundingClientRect | null
38
41
 
39
42
  setClass(name: string, enabled: boolean): this
40
43
  toggleClass(name: string): this
@@ -1,21 +1,60 @@
1
- import { type ElementStyle, type DrawableStyle, type Style, type StyleFn, type AppearStyle, type DisappearStyle, Element, type TextStyle, type Color, type OnLayoutCallback, type BaseStyle, type AnimateStyle } from "./UINode";
2
-
3
- type InputStyle = { placeholder?: string, placeholderColor?: Color, type?: "text" | "password" | "search" }
1
+ import { type ElementStyle, type DrawableStyle, type Style, type StyleFn, type AppearStyle, type DisappearStyle, Element, type TextStyle, type Color, type OnLayoutCallback, type BoundingClientRect, type BaseStyle, type AnimateStyle } from "./UINode";
2
+
3
+ type InputStyle = {
4
+ placeholder?: string,
5
+ placeholderColor?: Color,
6
+ /**
7
+ * Keyboard hint, NOT a validator — `number` does not block pasted letters; validate in your own
8
+ * onChange. The phone-pad value is `phone` (LeCodes has no `tel`).
9
+ */
10
+ type?: "text" | "password" | "search" | "phone" | "email" | "number" | "decimal" | "url",
11
+ /**
12
+ * What the layout does while the keyboard is up (decided by the FOCUSED input). `true` (default):
13
+ * the layout viewport shrinks to the area above the keyboard — one relayout. `false`: the keyboard
14
+ * overlays the UI with NO layout recalculation — for chat composers that manage their own inset
15
+ * (pair with `app.keyboardHeight` / the app `"keyboard"` event) and full-screen canvases where a
16
+ * reflow is worse than overlap.
17
+ */
18
+ keyboardShrink?: boolean,
19
+ /** Hard cap on the text length — enforced by the host while typing/pasting. */
20
+ maxLength?: number,
21
+ /** Auto-capitalization hint; omitted = platform default. */
22
+ autocapitalize?: "none" | "words" | "sentences" | "characters",
23
+ /** Enable/disable the platform autocorrection & suggestions; omitted = platform default. */
24
+ autocorrect?: boolean,
25
+ }
4
26
  export type UIInputStyle = ElementStyle & DrawableStyle & TextStyle & InputStyle
5
27
 
28
+ /** UIInput-only (a UITextArea's Enter is a newline, period): the return-key label. */
29
+ type SubmitStyle = {
30
+ /**
31
+ * Return-key label. On submit the keyboard dismisses UNLESS `enterKey` is `"next"` — then the
32
+ * app is expected to call `otherInput.focus()` from `onSubmit`, so the keyboard never blinks.
33
+ */
34
+ enterKey?: "done" | "go" | "next" | "search" | "send",
35
+ }
36
+
6
37
  export interface UIInput {
7
38
  readonly type: "input",
8
39
 
9
40
  value: string
10
41
 
11
- style: Style<this, UIInputStyle & { onFocused?: UIInputStyle & { duration?: number } }>,
42
+ style: Style<this, UIInputStyle & SubmitStyle & { onFocused?: UIInputStyle & { duration?: number } }>,
12
43
  animateTo: AnimateStyle<this, DrawableStyle & BaseStyle>
13
44
  animateFrom: AnimateStyle<this, DrawableStyle & BaseStyle>
14
45
 
15
46
  onChange(callback: (value: string) => void): UIInput
16
47
  onFocus(callback: () => void): UIInput
17
48
  onBlur(callback: () => void): UIInput
49
+ /** The keyboard's return key was pressed. `value` is the current text. See `enterKey`. */
50
+ onSubmit(callback: (value: string) => void): UIInput
18
51
  onLayout(onLayout: OnLayoutCallback): this
52
+ getBoundingClientRect(): BoundingClientRect | null
53
+
54
+ /** Focus the input (opens the keyboard). No-op before mount. */
55
+ focus(): void
56
+ /** Release focus (dismisses the keyboard when this input held it). */
57
+ blur(): void
19
58
 
20
59
  setClass(name: string, enabled: boolean): this
21
60
  toggleClass(name: string): this
@@ -39,6 +78,11 @@ export interface UITextArea {
39
78
  onFocus(callback: () => void): UITextArea
40
79
  onBlur(callback: () => void): UITextArea
41
80
 
81
+ /** Focus the text area (opens the keyboard). No-op before mount. */
82
+ focus(): void
83
+ /** Release focus (dismisses the keyboard when this text area held it). */
84
+ blur(): void
85
+
42
86
  setClass(name: string, enabled: boolean): this
43
87
  toggleClass(name: string): this
44
88
  hasClass(name: string): boolean
@@ -46,10 +90,11 @@ export interface UITextArea {
46
90
  }
47
91
 
48
92
  class InputElement<T extends "input" | "textarea"> extends Element<T> {
49
-
93
+
50
94
  readonly cl: Function[] = []
51
95
  readonly fl: Function[] = []
52
96
  readonly bl: Function[] = []
97
+ readonly sl: Function[] = []
53
98
 
54
99
  get value() {
55
100
  return _creatorUI.getTextValue(this._id)
@@ -60,6 +105,12 @@ class InputElement<T extends "input" | "textarea"> extends Element<T> {
60
105
  }
61
106
  }
62
107
 
108
+ // `phone` is SDK-surface only — hosts (and the web's verbatim `el.type`) keep HTML's `tel` as
109
+ // the wire format, so the rename never reaches them.
110
+ _mapStyle(style: any): void {
111
+ if (style.type === "phone") style.type = "tel"
112
+ }
113
+
63
114
  onChange (callback: (value: string) => void): this {
64
115
  this.cl.push(callback)
65
116
  return this
@@ -72,17 +123,33 @@ class InputElement<T extends "input" | "textarea"> extends Element<T> {
72
123
  this.bl.push(callback)
73
124
  return this
74
125
  }
126
+ onSubmit(callback: (value: string) => void): this {
127
+ this.sl.push(callback)
128
+ return this
129
+ }
130
+
131
+ // Focus rides the existing value channel (updateNode "focus") — no new ABI method.
132
+ focus(): void {
133
+ if (this._id !== 0) {
134
+ _creatorUI.updateNode(this._id, "focus", true)
135
+ }
136
+ }
137
+ blur(): void {
138
+ if (this._id !== 0) {
139
+ _creatorUI.updateNode(this._id, "focus", false)
140
+ }
141
+ }
75
142
  }
76
143
 
77
144
  export function UIInput(): UIInput;
78
145
  export function UIInput(style: UIInputStyle): UIInput;
79
146
  export function UIInput(style?: UIInputStyle): UIInput {
80
- return new InputElement("input", style ?? {})
147
+ // One text line as the floor — an unstyled input must never collapse to 0 height.
148
+ return new InputElement("input", { minHeight: "1.2em", ...style })
81
149
  }
82
150
 
83
151
  export function UITextArea(): UITextArea;
84
152
  export function UITextArea(style: UIInputStyle): UITextArea;
85
153
  export function UITextArea(style?: UIInputStyle): UITextArea {
86
- return new InputElement("textarea", style ?? {})
154
+ return new InputElement("textarea", { minHeight: "1.2em", ...style })
87
155
  }
88
-
@@ -0,0 +1,149 @@
1
+ import { type UIWidget, type UIWidgetStyle, WidgetElement } from "./UIWidget"
2
+ import type { BaseStyle, ChildrenFn, Color, DrawableStyle, UINodeChild } from "./UINode"
3
+
4
+ export type UIModalStyle = UIWidgetStyle
5
+
6
+ /**
7
+ * The modal's hidden pose: `show()` animates from it, `hide()` animates to it. `duration` (ms)
8
+ * applies to both directions.
9
+ */
10
+ export type UIModalTransition = DrawableStyle & BaseStyle & {
11
+ overlayColor?: Color | null,
12
+ duration?: number,
13
+ }
14
+
15
+ const MODAL_ANIM_MS = 200
16
+
17
+ /**
18
+ * A dialog: a `UIWidget` with the modal boilerplate built in. Comes with a scrim
19
+ * (`overlayColor: "rgba(0, 0, 0, 0.5)"` unless overridden), animates on `show()`/`hide()`
20
+ * (a 200 ms fade by default — swap the pose via `transition()`; the exit plays with
21
+ * `commit: false`, so the style is intact for the next show), and closes itself on a scrim tap
22
+ * or the Android back button (disable via `dismissible(false)`). Everything else is a plain
23
+ * widget — same styling, children, touch and `attachTo()` surface; create it **once** at module
24
+ * scope and reuse it.
25
+ */
26
+ export interface UIModal extends UIWidget {
27
+ /** Mount as an overlay and play the entrance transition. No-op while already open. */
28
+ show(): void,
29
+ /** Play the exit transition, then unmount. No-op while closed or already closing. */
30
+ hide(): void,
31
+ /** True from `show()` until `hide()` starts (already false during the exit animation). */
32
+ readonly isOpen: boolean,
33
+
34
+ /**
35
+ * Replace the show/hide animation. `hidden` is the off-screen pose — `show()` animates from
36
+ * it, `hide()` animates to it. Default `{ opacity: 0, duration: 200 }` (a fade); the pose
37
+ * replaces it wholesale, so a slide without a fade is just the slide. The scrim keeps its own
38
+ * fade unless the pose sets `overlayColor` itself.
39
+ *
40
+ * ```ts
41
+ * sheet.transition({ transform: "translateY(480px)", duration: 250 }) // bottom sheet
42
+ * ```
43
+ */
44
+ transition(hidden: UIModalTransition): this,
45
+
46
+ /** Called when `show()` mounts the modal. */
47
+ onOpen(callback: () => void): this,
48
+ /** Called when the modal starts closing — scrim tap, back button, or a `hide()` call. */
49
+ onClose(callback: () => void): this,
50
+ /** `dismissible(false)` keeps scrim taps and the back button from closing the modal
51
+ * (forced-choice dialogs). Default `true`. */
52
+ dismissible(enabled: boolean): this,
53
+
54
+ append(...nodes: UINodeChild[]): this
55
+ insert(index: number, ...nodes: UINodeChild[]): this
56
+ remove(...nodes: UINodeChild[]): this
57
+ }
58
+
59
+ export class ModalElement extends WidgetElement {
60
+ _open = false
61
+ _dismissible = true
62
+ _transition: UIModalTransition = { opacity: 0 }
63
+ _closeTimer?: ReturnType<typeof setTimeout>
64
+ readonly _openListeners: (() => void)[] = []
65
+ readonly _closeListeners: (() => void)[] = []
66
+
67
+ constructor(style: UIModalStyle, children: UINodeChild[] | ChildrenFn) {
68
+ super("widget", { overlayColor: "rgba(0, 0, 0, 0.5)", ...style }, children)
69
+ this.onOverlayTap(() => { if (this._dismissible) this.hide() })
70
+ this.onBackPressed(() => { if (this._dismissible) this.hide() })
71
+ }
72
+
73
+ // The full animate argument: hidden pose + duration, plus the scrim fade — only when the
74
+ // modal actually has a scrim (overlayColor: null opts out; animating a null scrim would
75
+ // create the intercepting layer) and the pose doesn't drive the scrim itself.
76
+ private _pose(extra?: Record<string, unknown>): Record<string, unknown> {
77
+ const pose: Record<string, unknown> = { duration: MODAL_ANIM_MS, ...this._transition, ...extra }
78
+ const overlayColor = (this._style as UIModalStyle).overlayColor
79
+ if (pose.overlayColor === undefined && overlayColor !== undefined && overlayColor !== null) {
80
+ pose.overlayColor = "transparent"
81
+ }
82
+ return pose
83
+ }
84
+
85
+ private get _duration(): number {
86
+ return this._transition.duration ?? MODAL_ANIM_MS
87
+ }
88
+
89
+ transition(hidden: UIModalTransition): this {
90
+ this._transition = hidden
91
+ return this
92
+ }
93
+
94
+ show(): void {
95
+ if (this._open) return
96
+ if (this._closeTimer !== undefined) { // reopened mid exit: finish that hide now
97
+ clearTimeout(this._closeTimer)
98
+ this._closeTimer = undefined
99
+ super.hide()
100
+ }
101
+ this._open = true
102
+ super.show()
103
+ this.animateFrom(this._pose())
104
+ for (const callback of this._openListeners) callback()
105
+ }
106
+
107
+ hide(): void {
108
+ if (!this._open) return
109
+ this._open = false
110
+ this.animateTo(this._pose({ commit: false }))
111
+ this._closeTimer = setTimeout(() => { this._closeTimer = undefined; super.hide() }, this._duration)
112
+ for (const callback of this._closeListeners) callback()
113
+ }
114
+
115
+ get isOpen(): boolean {
116
+ return this._open
117
+ }
118
+
119
+ onOpen(callback: () => void): this {
120
+ this._openListeners.push(callback)
121
+ return this
122
+ }
123
+ onClose(callback: () => void): this {
124
+ this._closeListeners.push(callback)
125
+ return this
126
+ }
127
+ dismissible(enabled: boolean): this {
128
+ this._dismissible = enabled
129
+ return this
130
+ }
131
+ }
132
+
133
+ export function UIModal(): UIModal;
134
+ export function UIModal(children: UINodeChild[] | ChildrenFn): UIModal;
135
+ export function UIModal(style: UIModalStyle): UIModal;
136
+ export function UIModal(style: UIModalStyle, children: UINodeChild[] | ChildrenFn): UIModal;
137
+ export function UIModal(...args: [] | [UINodeChild[] | ChildrenFn | UIModalStyle] | [UIModalStyle, children: UINodeChild[] | ChildrenFn]): UIModal {
138
+ if (args.length === 0) {
139
+ return new ModalElement({}, [])
140
+ } else if (args.length === 1) {
141
+ if (Array.isArray(args[0]) || typeof args[0] === "function") {
142
+ return new ModalElement({}, args[0])
143
+ } else {
144
+ return new ModalElement(args[0], [])
145
+ }
146
+ } else {
147
+ return new ModalElement(args[0], args[1])
148
+ }
149
+ }