lecodes-cli 0.10.4 → 0.12.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 (54) hide show
  1. package/README.md +1 -1
  2. package/dist/index.js +48887 -6261
  3. package/package.json +4 -3
  4. package/runtime/scene-harness.json +1 -1
  5. package/runtime/sdk/compile/aspectMacro.ts +42 -0
  6. package/runtime/sdk/compile/assetIconMacro.ts +382 -0
  7. package/runtime/sdk/compile/assetMacro.ts +45 -0
  8. package/runtime/sdk/compile/assetName.ts +50 -0
  9. package/runtime/sdk/compile/bundler.ts +244 -0
  10. package/runtime/sdk/compile/compileProject.ts +106 -0
  11. package/runtime/sdk/compile/detectEntry.ts +125 -0
  12. package/runtime/sdk/compile/fontMacro.ts +459 -0
  13. package/runtime/sdk/compile/fontRegistry.ts +78 -0
  14. package/runtime/sdk/compile/header.ts +55 -0
  15. package/runtime/sdk/compile/index.ts +53 -0
  16. package/runtime/sdk/compile/libraryImports.ts +52 -0
  17. package/runtime/sdk/compile/sceneEditor.ts +78 -0
  18. package/runtime/sdk/compile/sfnt.ts +98 -0
  19. package/runtime/sdk/compile/sourcemap.ts +25 -0
  20. package/runtime/sdk/g2/Node2D.ts +3 -0
  21. package/runtime/sdk/g2/SpriteAnimation.ts +101 -20
  22. package/runtime/sdk/g2/SpriteSheet.ts +166 -0
  23. package/runtime/sdk/g2/Tileset.ts +71 -0
  24. package/runtime/sdk/g2/autotile.ts +394 -0
  25. package/runtime/sdk/g2/cells.ts +91 -0
  26. package/runtime/sdk/g2/defineScene2d.ts +381 -0
  27. package/runtime/sdk/g2/scenarios2d.ts +69 -0
  28. package/runtime/sdk/inject.ts +18 -0
  29. package/runtime/sdk/runtime/app.ts +21 -3
  30. package/runtime/sdk/scene/defineScene.ts +10 -70
  31. package/runtime/sdk/scene/grammar.ts +85 -0
  32. package/runtime/sdk/ui/NativeView.ts +9 -12
  33. package/runtime/sdk/ui/UI.ts +9 -1
  34. package/runtime/sdk/ui/UIBottomSheet.ts +10 -20
  35. package/runtime/sdk/ui/UIButton.ts +19 -36
  36. package/runtime/sdk/ui/UIContainer.ts +41 -103
  37. package/runtime/sdk/ui/UIImage.ts +10 -18
  38. package/runtime/sdk/ui/UIInput.ts +14 -26
  39. package/runtime/sdk/ui/UIModal.ts +10 -20
  40. package/runtime/sdk/ui/UINode.ts +142 -36
  41. package/runtime/sdk/ui/UIPager.ts +29 -28
  42. package/runtime/sdk/ui/UIPopover.ts +10 -16
  43. package/runtime/sdk/ui/UIScreen.ts +23 -40
  44. package/runtime/sdk/ui/UIScrollable.ts +37 -47
  45. package/runtime/sdk/ui/UISpacer.ts +4 -8
  46. package/runtime/sdk/ui/UITabs.ts +175 -0
  47. package/runtime/sdk/ui/UIText.ts +7 -13
  48. package/runtime/sdk/ui/UIVideo.ts +11 -13
  49. package/runtime/sdk/ui/UIVirtualizedList.ts +43 -49
  50. package/runtime/sdk/ui/UIWidget.ts +24 -36
  51. package/runtime/sdk/ui/fonts.ts +3 -0
  52. package/runtime/sdk/ui/router.ts +13 -0
  53. package/runtime/sdk/ui/theme.ts +21 -30
  54. package/runtime/sdk-types.json +1 -1
@@ -1,46 +1,38 @@
1
1
  import type { UIContainerStyle } from "./UIContainer"
2
- import { type Classes, type Style, type AnimateStyle, type UINodeChild, type ChildrenFn, type OnLayoutCallback, type BoundingClientRect, type Color, ContainerElement } from "./UINode"
2
+ import { type UIChildArg, type UIContainerBase, type UINodeChild, type ChildrenFn, type Color, ContainerElement, buildUI } from "./UINode"
3
3
 
4
- // refreshControlColor tints the native pull-to-refresh spinner (pairs with onRefresh).
5
- // keyboardDismissMode: how a scroll gesture in THIS scrollable dismisses the keyboard.
6
- // "interactive" (default) — dragging over the keyboard slides it away (chat composer feel);
7
- // "scroll" — any drag dismisses the moment it starts (search-results feel);
8
- // "none" — scrolling never dismisses (tap-outside policy still applies).
9
- // snap: paging — when the drag ends, rest on a direct child's boundary along the scroll axis
10
- // (a carousel/pager is a horizontal scrollable of full-width children + snap:"start"). The value
11
- // picks where the child sits in the viewport; "none" (default) is free scrolling. Snap targets
12
- // are the children themselves, so item widths can differ (card-carousel-with-peek).
13
- export type ScrollStyle = { showScrollbar?: boolean, overscrollMode?: "none" | "absorb" | "default", refreshControlColor?: Color, keyboardDismissMode?: "interactive" | "scroll" | "none", snap?: "none" | "start" | "center" | "end" }
4
+ export type ScrollStyle = {
5
+ showScrollbar?: boolean,
6
+ overscrollMode?: "none" | "absorb" | "default",
7
+ /** Tint of the native pull-to-refresh spinner (pairs with `onRefresh`). */
8
+ refreshControlColor?: Color,
9
+ /** How a scroll gesture in THIS scrollable dismisses the keyboard: `"interactive"` (default) —
10
+ * dragging over the keyboard slides it away (chat feel); `"scroll"` — any drag dismisses
11
+ * immediately; `"none"` — never (tap-outside policy still applies). */
12
+ keyboardDismissMode?: "interactive" | "scroll" | "none",
13
+ /** Paging: when the drag ends, rest on a direct child's boundary along the scroll axis — the
14
+ * value picks where the child sits in the viewport; `"none"` (default) is free scrolling. Snap
15
+ * targets are the children themselves, so item widths can differ (card-carousel-with-peek); a
16
+ * carousel is a horizontal scrollable of full-width children + `snap: "start"`. */
17
+ snap?: "none" | "start" | "center" | "end"
18
+ }
14
19
 
15
20
  export type UIScrollableStyle = UIContainerStyle & ScrollStyle & { scrollDirection?: "horizontal" | "vertical" }
16
21
 
17
- export interface UIScrollable {
22
+ /** A scroll container — vertical by default, `scrollDirection: "horizontal"` for carousels/chip
23
+ * rows. The scrolling body of a screen (screens themselves never scroll). */
24
+ export interface UIScrollable extends UIContainerBase<UIScrollableStyle> {
18
25
  readonly type: "scrollable",
19
- style: Style<this, UIScrollableStyle>,
20
- animateTo: AnimateStyle<this, UIScrollableStyle>,
21
- animateFrom: AnimateStyle<this, UIScrollableStyle>,
22
-
23
- append(...nodes: UINodeChild[]): UIScrollable
24
- insert(index: number, ...nodes: UINodeChild[]): UIScrollable
25
- remove(...nodes: UINodeChild[]): UIScrollable
26
- setContent(nodes: UINodeChild[] | ChildrenFn): this
27
-
28
- onLayout(onLayout: OnLayoutCallback): this
29
- getBoundingClientRect(): BoundingClientRect | null
30
-
31
- /** Style-class proxy — read `el.class.checked`, set `el.class.checked = true`, toggle with
32
- * `!el.class.checked`, bind `el.class.done = () => sig.value`, batch/chain `el.class({ … })`.
33
- * Classes cascade to descendants. See {@link Classes}. */
34
- readonly class: Classes<this>
35
26
 
27
+ /** Scroll position changed (px from the start edge). */
36
28
  onScroll(callback: (scrollPosition: number) => void): UIScrollable,
29
+ /** The scroll gesture ended (finger lifted). */
37
30
  onScrollRelease(callback: () => void): UIScrollable,
31
+ /** Pulled past the content edge by `delta` px. */
38
32
  onOverscroll(callback: (delta: number) => void): UIScrollable
39
33
  /** Pull-to-refresh: the spinner stays until the returned promise settles. Attach BEFORE the
40
34
  * node mounts (hosts read it at creation). Vertical only; native hosts — no-op on web. */
41
35
  onRefresh(callback: () => void | Promise<void>): UIScrollable
42
-
43
- readonly children: UINodeChild[]
44
36
  }
45
37
 
46
38
  class ScrollableElement extends ContainerElement<"scrollable"> {
@@ -76,22 +68,20 @@ class ScrollableElement extends ContainerElement<"scrollable"> {
76
68
  }
77
69
  }
78
70
 
79
- export function UIScrollable(): UIScrollable
71
+ // flexShrink: 1 default (SDK-owned, like UIRow's flexDirection): without it a scrollable in a
72
+ // flex parent grows to its content height and overflows instead of scrolling.
73
+ const SCROLLABLE_DEFAULTS = { flexShrink: 1 } as const
74
+ const makeScrollable = (style: any, children: any) => new ScrollableElement("scrollable", style, children)
75
+
76
+ /** @internal Compiler fast path (chisel `flatten_ui` raw lowering) — see UIContainer. */
77
+ export const __UIScrollable = (children: UINodeChild[] | ChildrenFn): UIScrollable => makeScrollable({ ...SCROLLABLE_DEFAULTS }, children)
78
+
79
+ /** Create a scroll container (same argument forms as `UIColumn`). As a screen body, pair with
80
+ * `flexGrow: 1`. */
81
+ export function UIScrollable(...children: UIChildArg[]): UIScrollable
80
82
  export function UIScrollable(children: UINodeChild[] | ChildrenFn): UIScrollable
81
- export function UIScrollable(style: UIScrollableStyle): UIScrollable
82
- export function UIScrollable(style: UIScrollableStyle, children: UINodeChild[]): UIScrollable
83
- export function UIScrollable(...args: [] | [UINodeChild[] | ChildrenFn | UIScrollableStyle] | [UIScrollableStyle, children: UINodeChild[] | ChildrenFn]): UIScrollable {
84
- // flexShrink: 1 default (SDK-owned, like UIRow's flexDirection): without it a scrollable in a
85
- // flex parent grows to its content height and overflows instead of scrolling.
86
- if (args.length === 0) {
87
- return new ScrollableElement("scrollable", { flexShrink: 1 }, [])
88
- } else if (args.length === 1) {
89
- if (Array.isArray(args[0]) || typeof args[0] === "function") {
90
- return new ScrollableElement("scrollable", { flexShrink: 1 }, args[0])
91
- } else {
92
- return new ScrollableElement("scrollable", { flexShrink: 1, ...args[0] }, [])
93
- }
94
- } else {
95
- return new ScrollableElement("scrollable", { flexShrink: 1, ...args[0] }, args[1])
96
- }
83
+ /** @deprecated Style as the first argument is legacy — use chained `.style({...})`. */
84
+ export function UIScrollable(style: UIScrollableStyle, children?: UINodeChild[] | ChildrenFn): UIScrollable
85
+ export function UIScrollable(...args: any[]): UIScrollable {
86
+ return buildUI(args, SCROLLABLE_DEFAULTS, makeScrollable)
97
87
  }
@@ -1,16 +1,12 @@
1
- import { Element, type ElementStyle, type Classes, type Style, type StyleFn } from "./UINode"
1
+ import { Element, type ElementStyle, type UIElementBase } from "./UINode"
2
2
 
3
3
  export type UISpacerStyle = ElementStyle
4
4
 
5
- export interface UISpacer {
5
+ /** A flexible gap — `flexGrow: 1` by default, pushes its siblings apart. */
6
+ export interface UISpacer extends UIElementBase<UISpacerStyle> {
6
7
  readonly type: "spacer",
7
- style: Style<this, UISpacerStyle>,
8
-
9
- /** Style-class proxy — read `el.class.checked`, set `el.class.checked = true`, toggle with
10
- * `!el.class.checked`, bind `el.class.done = () => sig.value`, batch/chain `el.class({ … })`.
11
- * Classes cascade to descendants. See {@link Classes}. */
12
- readonly class: Classes<this>
13
8
  }
9
+ /** Create a flexible gap (`flexGrow: 1`) — e.g. between a header's title and its actions. */
14
10
  export function UISpacer(): UISpacer
15
11
  export function UISpacer(style: UISpacerStyle): UISpacer
16
12
  export function UISpacer(style?: UISpacerStyle): UISpacer {
@@ -0,0 +1,175 @@
1
+ import { PagerElement, type UIPager } from "./UIPager"
2
+ import { ScreenElement, type UIScreen } from "./UIScreen"
3
+ import { UIButton } from "./UIButton"
4
+ import { UIRow, UIColumn, type UIRow as UIRowType } from "./UIContainer"
5
+ import { UIText } from "./UIText"
6
+ import { UIImage, type ImageSource } from "./UIImage"
7
+
8
+ // UITabs — the standard bottom-tab shell, prewired: a UIScreen composing [ UIPager (flexGrow: 1),
9
+ // tab bar ]. Pure SDK composition over the pager — zero new ABI, works wherever UIPager does.
10
+ // The build-mode twin of the design scaffold's `defineTabs` (lecodes-design/tabbar-surface.md):
11
+ // same keyed config, same `tab-<id>` button names, same theme variables with the same fallbacks,
12
+ // so a designed mockup and the built app render identically.
13
+
14
+ /** Active/inactive/bar colors ride the theme — one `theme({ primaryColor, mutedColor, tabbarBg })`
15
+ * call restyles the bar app-wide; the fallbacks keep an unthemed app looking right. */
16
+ const ACTIVE = "var(--primaryColor, #5b8cff)"
17
+ const INACTIVE = "var(--mutedColor, #8a919e)"
18
+ const BAR_BG = "var(--tabbarBg, #15171c)"
19
+ const BAR_BORDER = "var(--tabbarBorder, #23262e)"
20
+ const SCREEN_BG = "var(--screenBg, #101114)"
21
+ const BADGE_BG = "var(--badgeColor, #ff453a)"
22
+
23
+ export type UITabDef = {
24
+ label: string,
25
+ /** Tab icon — typically `assetIcon("lucide:…")`. Tinted active/inactive by the bar. */
26
+ icon?: ImageSource,
27
+ /** The tab's root screen. Tabs are fixed at construction (like UIPager's). */
28
+ screen: UIScreen,
29
+ }
30
+
31
+ /**
32
+ * A bottom-tab app shell: swipeable tabs (a `UIPager`) plus a themed tab bar, as one
33
+ * `UIScreen` — present it directly (`Router.init(tabs)`). Keys are tab ids, in tab order:
34
+ *
35
+ * ```ts
36
+ * const tabs = UITabs({
37
+ * home: { label: "Home", icon: assetIcon("lucide:house"), screen: homeScreen },
38
+ * profile: { label: "Profile", icon: assetIcon("lucide:user"), screen: profileScreen },
39
+ * })
40
+ * Router.init(tabs)
41
+ * ```
42
+ *
43
+ * In-tab navigation stays the pager's: `UIPager.push(detail)` from any screen (bar stays);
44
+ * `Router.push` opens above the shell (bar covered). Restyle via `theme()` (primaryColor,
45
+ * mutedColor, tabbarBg, …) or the `bar` handle; for a fully custom bar build on `UIPager`.
46
+ */
47
+ export interface UITabs extends UIScreen {
48
+ /** The active tab id. */
49
+ readonly tab: string,
50
+ /** Switch tabs (instant, the tab-bar convention; `animated: true` slides). */
51
+ select(id: string, animated?: boolean): this,
52
+ /** Fires when the active tab changes and has settled — a bar tap, swipe, or `select()`. */
53
+ onSelect(callback: (id: string, index: number) => void): this,
54
+ /** Show a badge on a tab: `true` = dot, a number/string = count pill, `false`/`null`/`0` clears. */
55
+ badge(id: string, value: number | string | boolean | null): this,
56
+ /** The pager under the bar — in-tab stacks (`push`/`pop`/`depth`). */
57
+ readonly pager: UIPager,
58
+ /** The bar row (named "tabbar") — for restyling beyond the theme variables. */
59
+ readonly bar: UIRowType,
60
+ }
61
+
62
+ export class TabsElement extends ScreenElement {
63
+ private readonly ids: string[]
64
+ private readonly _pagerEl: PagerElement
65
+ private readonly _bar: any
66
+ private readonly _icons: (any | null)[] = []
67
+ private readonly _labels: any[] = []
68
+ private readonly _badges: any[] = []
69
+ private readonly _badgeTexts: any[] = []
70
+ private readonly _buttons: any[] = []
71
+ private readonly _selectListeners: ((id: string, index: number) => void)[] = []
72
+
73
+ constructor(defs: Record<string, UITabDef>) {
74
+ super("screen", { bgColor: SCREEN_BG }, [])
75
+ this.ids = Object.keys(defs)
76
+ if (this.ids.length === 0) console.warn("UITabs: no tabs declared")
77
+
78
+ this._pagerEl = new PagerElement({ flexGrow: 1 }, this.ids.map((id) => defs[id].screen))
79
+
80
+ const buttons = this.ids.map((id, i) => {
81
+ const def = defs[id]
82
+ const icon = def.icon ? UIImage(def.icon).style({ width: 22, height: 22, tintColor: INACTIVE }) : null
83
+ const label = UIText(def.label).style({ fontSize: 10, color: INACTIVE })
84
+ const badgeText = UIText("").style({ fontSize: 9, fontWeight: 700, color: "white" })
85
+ const badge = UIRow(badgeText).style({
86
+ position: "absolute", top: -3, right: -6, display: "none",
87
+ bgColor: BADGE_BG, borderRadius: 99, justifyContent: "center", alignItems: "center",
88
+ })
89
+ // The badge anchors to the icon (or the label when there is none) — its wrap must not clip.
90
+ const anchor = UIColumn(icon ?? label, badge).style({ overflow: "visible", alignItems: "center" })
91
+ this._icons.push(icon)
92
+ this._labels.push(label)
93
+ this._badges.push(badge)
94
+ this._badgeTexts.push(badgeText)
95
+ const button = UIButton(anchor, icon ? label : null)
96
+ .style({ name: `tab-${id}`, flexDirection: "column", gap: 3, flex: 1, pt: 8, pb: 6 })
97
+ .onClick(() => this.select(id))
98
+ this._buttons.push(button)
99
+ return button
100
+ })
101
+
102
+ this._bar = UIRow(buttons).style({
103
+ name: "tabbar", bgColor: BAR_BG, pb: "comfort-bottom",
104
+ borderTopWidth: 1, borderTopColor: BAR_BORDER,
105
+ })
106
+
107
+ this._pagerEl.onSelect((i) => {
108
+ this._applyActive(i)
109
+ for (const cb of this._selectListeners) cb(this.ids[i], i)
110
+ })
111
+ this._applyActive(0)
112
+ this.append(this._pagerEl as any, this._bar)
113
+ }
114
+
115
+ private _applyActive(index: number): void {
116
+ for (let i = 0; i < this.ids.length; i++) {
117
+ const on = i === index
118
+ const color = on ? ACTIVE : INACTIVE
119
+ // Imperative writes carry the visual on every host; the class is the styling hook — custom
120
+ // content inside a tab button can declare `$active` blocks (class state cascades down).
121
+ this._buttons[i].class.active = on
122
+ if (this._icons[i]) this._icons[i].style.tintColor = color
123
+ this._labels[i].style.color = color
124
+ }
125
+ }
126
+
127
+ get tab(): string {
128
+ return this.ids[this._pagerEl.index]
129
+ }
130
+ get pager(): any {
131
+ return this._pagerEl
132
+ }
133
+ get bar(): any {
134
+ return this._bar
135
+ }
136
+
137
+ select(id: string, animated = false): this {
138
+ const index = this.ids.indexOf(id)
139
+ if (index < 0) {
140
+ console.warn(`UITabs.select: unknown tab "${id}"`)
141
+ return this
142
+ }
143
+ this._pagerEl.select(index, animated)
144
+ return this
145
+ }
146
+
147
+ onSelect(callback: (id: string, index: number) => void): this {
148
+ this._selectListeners.push(callback)
149
+ return this
150
+ }
151
+
152
+ badge(id: string, value: number | string | boolean | null | undefined): this {
153
+ const i = this.ids.indexOf(id)
154
+ if (i < 0) {
155
+ console.warn(`UITabs.badge: unknown tab "${id}"`)
156
+ return this
157
+ }
158
+ if (value === null || value === undefined || value === false || value === 0 || value === "") {
159
+ this._badges[i].style.display = "none"
160
+ } else if (value === true) {
161
+ this._badgeTexts[i].text = ""
162
+ this._badges[i].style({ display: "flex", width: 9, height: 9, minWidth: 0, px: 0 })
163
+ } else {
164
+ this._badgeTexts[i].text = String(value)
165
+ this._badges[i].style({ display: "flex", width: "auto", height: 14, minWidth: 14, px: 4 })
166
+ }
167
+ return this
168
+ }
169
+ }
170
+
171
+ /** Create the standard bottom-tab shell — see {@link UITabs}. Keys are tab ids in tab order;
172
+ * each tab needs a `label` and its root `screen` (plus an optional `icon`). */
173
+ export function UITabs(tabs: Record<string, UITabDef>): UITabs {
174
+ return new TabsElement(tabs) as unknown as UITabs
175
+ }
@@ -1,22 +1,13 @@
1
1
  import { createBinding } from "../core/signals"
2
- import { Element, type AnimateStyle, type BaseStyle, type DrawableStyle, type ElementStyle, type OnLayoutCallback, type BoundingClientRect, type Classes, type Style, type TextStyle } from "./UINode"
2
+ import { Element, type BaseStyle, type DrawableStyle, type ElementStyle, type TextStyle, type UIElementBase } from "./UINode"
3
3
 
4
4
  export type UITextStyle = ElementStyle & TextStyle & DrawableStyle
5
5
 
6
- export interface UIText {
6
+ /** A text label. */
7
+ export interface UIText extends UIElementBase<UITextStyle, DrawableStyle & BaseStyle & TextStyle> {
7
8
  readonly type: "text",
9
+ /** The displayed string — writable at any time. */
8
10
  text: string,
9
- style: Style<this, UITextStyle>,
10
- animateTo: AnimateStyle<this, DrawableStyle & BaseStyle & TextStyle>
11
- animateFrom: AnimateStyle<this, DrawableStyle & BaseStyle & TextStyle>
12
-
13
- onLayout(onLayout: OnLayoutCallback): this
14
- getBoundingClientRect(): BoundingClientRect | null
15
-
16
- /** Style-class proxy — read `el.class.checked`, set `el.class.checked = true`, toggle with
17
- * `!el.class.checked`, bind `el.class.done = () => sig.value`, batch/chain `el.class({ … })`.
18
- * Classes cascade to descendants. See {@link Classes}. */
19
- readonly class: Classes<this>
20
11
  }
21
12
 
22
13
  export class TextElement extends Element<"text"> {
@@ -46,7 +37,10 @@ export class TextElement extends Element<"text"> {
46
37
  }
47
38
  }
48
39
 
40
+ /** Create a text label — a string, or `() => string` for reactive text that re-renders when a
41
+ * signal it read changes. */
49
42
  export function UIText(text: string | (() => string)): UIText;
43
+ /** @deprecated Style as the first argument is legacy — use chained `.style({...})`. */
50
44
  export function UIText(style: UITextStyle, text: string | (() => string)): UIText;
51
45
  export function UIText(...args: [string | (() => string)] | [UITextStyle, string | (() => string)]): UIText {
52
46
  if (args.length === 1) {
@@ -1,31 +1,27 @@
1
1
  import type { VideoPlayer } from "../runtime/media"
2
2
  import { _bumpNavEpoch, _navSupported, _setCurrent, Presentable, type PresentOptions } from "./presentable"
3
- import { Element, type AnimateStyle, type AppearStyle, type BaseStyle, type DisappearStyle, type DrawableStyle, type ElementStyle, type OnLayoutCallback, type BoundingClientRect, type Classes, type Style, type StyleFn } from "./UINode"
3
+ import { Element, type BaseStyle, type DrawableStyle, type ElementStyle, type UIElementBase } from "./UINode"
4
4
 
5
5
  export type UIVideoStyle = ElementStyle & DrawableStyle & { objectFit?: "cover" | "contain" | "fill" }
6
6
 
7
- export interface UIVideo {
7
+ /** A video surface showing a `VideoPlayer`'s output — embeddable in a screen AND
8
+ * fullscreen-presentable (`open()`), with seamless promotion between the two. */
9
+ export interface UIVideo extends UIElementBase<UIVideoStyle, DrawableStyle & BaseStyle> {
8
10
  readonly type: "video",
11
+ /** The player driving this surface (playback control lives on it). */
9
12
  player: VideoPlayer
10
- style: Style<this, UIVideoStyle>
11
- animateTo: AnimateStyle<this, DrawableStyle & BaseStyle>
12
- animateFrom: AnimateStyle<this, DrawableStyle & BaseStyle>
13
13
 
14
14
  /** Show fullscreen as the current destination. Promotion: if this video is mounted in a screen,
15
15
  * the SAME native player moves fullscreen — playback uninterrupted; a pop moves it back. */
16
16
  open(options?: PresentOptions): void
17
+ /** Dismiss if this is the visible destination. */
17
18
  close(): void
19
+ /** Became the visible destination. */
18
20
  onOpen(callback: () => void): this
21
+ /** Stopped being the visible destination. */
19
22
  onClose(callback: () => void): this
23
+ /** Hardware/system back while current. */
20
24
  onBackPressed(callback: () => void): this
21
-
22
- onLayout(onLayout: OnLayoutCallback): this
23
- getBoundingClientRect(): BoundingClientRect | null
24
-
25
- /** Style-class proxy — read `el.class.checked`, set `el.class.checked = true`, toggle with
26
- * `!el.class.checked`, bind `el.class.done = () => sig.value`, batch/chain `el.class({ … })`.
27
- * Classes cascade to descendants. See {@link Classes}. */
28
- readonly class: Classes<this>
29
25
  }
30
26
 
31
27
  class VideoElement extends Element<"video"> implements Presentable {
@@ -79,7 +75,9 @@ class VideoElement extends Element<"video"> implements Presentable {
79
75
  }
80
76
  }
81
77
 
78
+ /** Create a video node showing `player`'s output. */
82
79
  export function UIVideo(player: VideoPlayer): UIVideo
80
+ /** @deprecated Style as the first argument is legacy — use chained `.style({...})`. */
83
81
  export function UIVideo(style: UIVideoStyle, player: VideoPlayer): UIVideo
84
82
  export function UIVideo(...args: [VideoPlayer] | [UIVideoStyle, VideoPlayer]): UIVideo {
85
83
  if (args.length === 1) {
@@ -1,75 +1,78 @@
1
- import { Element, type Color, type DrawableStyle, type ElementStyle, type OnLayoutCallback, type BoundingClientRect, type PaddingStyle, type Classes, type Style, type UINodeChild } from "./UINode"
1
+ import { Element, type Color, type DrawableStyle, type ElementStyle, type UIElementBase, type UINodeChild } from "./UINode"
2
2
 
3
- // refreshControlColor tints the native pull-to-refresh spinner (pairs with onRefresh).
4
- // keyboardDismissMode: same key and semantics as UIScrollable's — hosts treat a vlist as a scroll
5
- // container, so it always honored this; only the type was missing. A chat is the reason it exists:
6
- // "none" keeps the composer's keyboard up no matter how the history is scrolled.
7
3
  export type UIVirtualizedListStyle = DrawableStyle & ElementStyle & {
4
+ /** Tint of the native pull-to-refresh spinner (pairs with `onRefresh`). */
8
5
  refreshControlColor?: Color,
6
+ /** Same key and semantics as UIScrollable's; `"none"` keeps a chat composer's keyboard up no
7
+ * matter how the history is scrolled. */
9
8
  keyboardDismissMode?: "interactive" | "scroll" | "none",
10
9
  }
11
10
 
12
11
  export interface VListConfig<T> {
13
- /** Стабильный уникальный ключ айтема */
12
+ /** Stable unique key of an item. */
14
13
  keyOf: (item: T) => string
15
- /** Фабрика поддерева. Вызывается при входе айтема в окно; поддерево живёт до выхода из окна */
14
+ /** Subtree factory — called when the item enters the window; the subtree lives until it
15
+ * leaves. */
16
16
  render: (item: T) => UINodeChild
17
- /**
18
- * Оценка высоты (px) до первого измерения. Число — или функция от айтема,
19
- * например для чата: msg => 40 + Math.ceil(msg.text.length / 35) * 20
20
- * Вычисляется один раз при добавлении айтема, в нативку уходит готовое число.
21
- */
17
+ /** Height estimate (px) used before the first measurement — a number, or a per-item function
18
+ * (chat: `msg => 40 + Math.ceil(msg.text.length / 35) * 20`). Computed once when the item is
19
+ * added. */
22
20
  estimatedHeight: number | ((item: T) => number)
23
- /** px вокруг вьюпорта, которые держим смонтированными. Default: высота вьюпорта */
21
+ /** Extra px around the viewport kept mounted. Default: one viewport height. */
24
22
  overscan?: number
25
- /** Чат-режим: первый layout проскроллен в конец, append при положении внизу автоскроллит */
23
+ /** Chat mode: the first layout starts scrolled to the end, and appends auto-scroll while the
24
+ * list sits at the bottom. */
26
25
  inverted?: boolean
27
26
  }
28
27
 
29
- export interface UIVirtualizedList<T = unknown> {
28
+ /** A windowed (virtualized) list: only the items near the viewport are mounted — for long feeds
29
+ * and chats. Items are keyed; feed it via `setData`/`append`/`prepend`. */
30
+ export interface UIVirtualizedList<T = unknown> extends UIElementBase<UIVirtualizedListStyle> {
30
31
  readonly type: "vlist"
31
- style: Style<this, UIVirtualizedListStyle>
32
32
 
33
- /** Полная замена данных. Нативка сама размонтирует исчезнувшие ключи через _syncWindow */
33
+ /** Replace the data wholesale — vanished keys are unmounted by the host's window sync. */
34
34
  setData(items: T[]): this
35
+ /** Append items to the end (auto-scrolls when `inverted` and resting at the bottom). */
35
36
  append(...items: T[]): this
36
- /** Вставка в начало БЕЗ прыжка скролла (компенсация на стороне нативки) */
37
+ /** Insert at the start WITHOUT a scroll jump (the host compensates the offset). */
37
38
  prepend(...items: T[]): this
38
- /** Обновить айтемы с теми же ключами: re-render, если смонтированы. Высота в кэше сохраняется до повторного измерения */
39
+ /** Update items with the same keys: mounted ones re-render; the cached height persists until
40
+ * re-measured. */
39
41
  update(...items: T[]): this
42
+ /** Remove items by key. */
40
43
  removeByKey(...keys: string[]): this
41
44
 
45
+ /** Number of data items (mounted or not). */
42
46
  readonly itemCount: number
47
+ /** The data item under `key`, if any. */
43
48
  getItem(key: string): T | undefined
44
49
 
50
+ /** Scroll to `offset` px. */
45
51
  scrollTo(offset: number, animated?: boolean): void
52
+ /** Scroll to the item with `key`. */
46
53
  scrollToKey(key: string, animated?: boolean): void
54
+ /** Scroll to the end of the content. */
47
55
  scrollToEnd(animated?: boolean): void
48
56
 
57
+ /** Scroll position changed (px from the start edge). */
49
58
  onScroll(callback: (scrollPosition: number) => void): this
50
- /** Pull-to-refresh: спиннер держится, пока не разрешится промис. Навесить ДО маунта
51
- * (нативка читает при создании ноды). Только native-хосты; на вебе no-op. */
59
+ /** Pull-to-refresh: the spinner stays until the returned promise settles. Attach BEFORE the
60
+ * node mounts (hosts read it at creation). Native hosts only — no-op on web. */
52
61
  onRefresh(callback: () => void | Promise<void>): this
53
- /** Порог в px от нижнего края контента. Срабатывает один раз до ухода от края (защёлка в нативке) */
62
+ /** Fires within `thresholdPx` of the content's bottom edge — once, re-armed after leaving the
63
+ * edge (host-side latch). Infinite-scroll loading. */
54
64
  onEndReached(thresholdPx: number, callback: () => void): this
55
- /** Порог от верхнего края — подгрузка истории в чате */
65
+ /** Same, for the top edge — chat history loading. */
56
66
  onStartReached(thresholdPx: number, callback: () => void): this
57
- onLayout(onLayout: OnLayoutCallback): this
58
- getBoundingClientRect(): BoundingClientRect | null
59
-
60
- /** Style-class proxy — read `el.class.checked`, set `el.class.checked = true`, toggle with
61
- * `!el.class.checked`, bind `el.class.done = () => sig.value`, batch/chain `el.class({ … })`.
62
- * Classes cascade to descendants. See {@link Classes}. */
63
- readonly class: Classes<this>
64
67
  }
65
68
 
66
69
  export class VListElement<T> extends Element<"vlist"> implements UIVirtualizedList<T> {
67
70
 
68
- // --- читается нативкой в createUINode (copyProperty / copyNodePtr) ---
69
- _overscan: number // -1 = "высота вьюпорта", резолвится в нативке
71
+ // --- read by native side in createUINode (copyProperty / copyNodePtr) ---
72
+ _overscan: number
70
73
  _inverted: boolean
71
- _keys: string[] = [] // начальное состояние до маунта ноды
72
- _estimates: number[] = [] // параллельно _keys
74
+ _keys: string[] = []
75
+ _estimates: number[] = []
73
76
  _endThreshold = 0
74
77
  _startThreshold = 0
75
78
 
@@ -123,12 +126,8 @@ export class VListElement<T> extends Element<"vlist"> implements UIVirtualizedLi
123
126
  this._keys = keys
124
127
  this._estimates = estimates
125
128
  if (this.isAttached) {
126
- // нативка диффает по ключам: исчезнувшие смонтированные ключи придут в _syncWindow как unmount
127
129
  _creatorUI.vlistSetKeys(this, keys, estimates)
128
- // setKeys диффает ТОЛЬКО по ключам — пережившие replace смонтированные айтемы сохраняют
129
- // старое поддерево. Если объект данных изменился, перерисовываем их (как update), иначе
130
- // строки остаются со старым содержимым. На вебе экран в истории держит айтемы
131
- // смонтированными, поэтому без этого повторный setData их не обновлял.
130
+
132
131
  for (const k of prevMounted) {
133
132
  if (this.mounted.has(k) && this.items.get(k) !== prevItems.get(k)) {
134
133
  _creatorUI.vlistInvalidate(this, k)
@@ -143,7 +142,6 @@ export class VListElement<T> extends Element<"vlist"> implements UIVirtualizedLi
143
142
  this._keys.push(...keys)
144
143
  this._estimates.push(...estimates)
145
144
  if (this.isAttached) {
146
- // inverted + позиция внизу → нативка автоскроллит к концу
147
145
  _creatorUI.vlistInsertKeys(this, -1, keys, estimates)
148
146
  }
149
147
  return this
@@ -154,7 +152,6 @@ export class VListElement<T> extends Element<"vlist"> implements UIVirtualizedLi
154
152
  this._keys.unshift(...keys)
155
153
  this._estimates.unshift(...estimates)
156
154
  if (this.isAttached) {
157
- // нативка компенсирует scrollY на сумму estimates — позиция не прыгает
158
155
  _creatorUI.vlistInsertKeys(this, 0, keys, estimates)
159
156
  }
160
157
  return this
@@ -166,8 +163,6 @@ export class VListElement<T> extends Element<"vlist"> implements UIVirtualizedLi
166
163
  if (!this.items.has(k)) continue
167
164
  this.items.set(k, item)
168
165
  if (this.isAttached && this.mounted.has(k)) {
169
- // unmount+mount, кэш высоты сохраняется до повторного измерения.
170
- // Несмонтированный айтем перерендерится сам при входе в окно
171
166
  _creatorUI.vlistInvalidate(this, k)
172
167
  }
173
168
  }
@@ -221,12 +216,9 @@ export class VListElement<T> extends Element<"vlist"> implements UIVirtualizedLi
221
216
  return this
222
217
  }
223
218
 
224
- /**
225
- * Единственный колбэк из нативки (через jsPtr, как у button).
226
- * Вызывается ТОЛЬКО когда окно пересекло границу — не на каждый кадр скролла.
227
- * Порядок: сначала unmount (нативка уже снесла поддеревья и обнулила _id,
228
- * здесь только чистим JS-ссылки), потом mount.
229
- */
219
+ /** @internal The one host→JS callback (via jsPtr, like button). Fires ONLY when the window
220
+ * crossed a boundary — not per scroll frame. Order: unmounts first (the host already tore the
221
+ * subtrees down and zeroed _id; only JS refs are cleaned here), then mounts. */
230
222
  _syncWindow(unmountKeys: string[], mountKeys: string[]): void {
231
223
  for (const k of unmountKeys) {
232
224
  this.mounted.delete(k)
@@ -242,6 +234,8 @@ export class VListElement<T> extends Element<"vlist"> implements UIVirtualizedLi
242
234
  }
243
235
  }
244
236
 
237
+ /** Create a virtualized list from a config (`keyOf` + `render` + `estimatedHeight` required),
238
+ * then feed it via `setData`/`append`/`prepend`. Give it a size (`flexGrow: 1` as a screen body). */
245
239
  export function UIVirtualizedList<T>(config: VListConfig<T>): UIVirtualizedList<T> {
246
240
  return new VListElement<T>(config)
247
241
  }