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.
- package/README.md +1 -1
- package/dist/index.js +48887 -6261
- package/package.json +4 -3
- package/runtime/scene-harness.json +1 -1
- package/runtime/sdk/compile/aspectMacro.ts +42 -0
- package/runtime/sdk/compile/assetIconMacro.ts +382 -0
- package/runtime/sdk/compile/assetMacro.ts +45 -0
- package/runtime/sdk/compile/assetName.ts +50 -0
- package/runtime/sdk/compile/bundler.ts +244 -0
- package/runtime/sdk/compile/compileProject.ts +106 -0
- package/runtime/sdk/compile/detectEntry.ts +125 -0
- package/runtime/sdk/compile/fontMacro.ts +459 -0
- package/runtime/sdk/compile/fontRegistry.ts +78 -0
- package/runtime/sdk/compile/header.ts +55 -0
- package/runtime/sdk/compile/index.ts +53 -0
- package/runtime/sdk/compile/libraryImports.ts +52 -0
- package/runtime/sdk/compile/sceneEditor.ts +78 -0
- package/runtime/sdk/compile/sfnt.ts +98 -0
- package/runtime/sdk/compile/sourcemap.ts +25 -0
- package/runtime/sdk/g2/Node2D.ts +3 -0
- package/runtime/sdk/g2/SpriteAnimation.ts +101 -20
- package/runtime/sdk/g2/SpriteSheet.ts +166 -0
- package/runtime/sdk/g2/Tileset.ts +71 -0
- package/runtime/sdk/g2/autotile.ts +394 -0
- package/runtime/sdk/g2/cells.ts +91 -0
- package/runtime/sdk/g2/defineScene2d.ts +381 -0
- package/runtime/sdk/g2/scenarios2d.ts +69 -0
- package/runtime/sdk/inject.ts +18 -0
- package/runtime/sdk/runtime/app.ts +21 -3
- package/runtime/sdk/scene/defineScene.ts +10 -70
- package/runtime/sdk/scene/grammar.ts +85 -0
- package/runtime/sdk/ui/NativeView.ts +9 -12
- package/runtime/sdk/ui/UI.ts +9 -1
- package/runtime/sdk/ui/UIBottomSheet.ts +10 -20
- package/runtime/sdk/ui/UIButton.ts +19 -36
- package/runtime/sdk/ui/UIContainer.ts +41 -103
- package/runtime/sdk/ui/UIImage.ts +10 -18
- package/runtime/sdk/ui/UIInput.ts +14 -26
- package/runtime/sdk/ui/UIModal.ts +10 -20
- package/runtime/sdk/ui/UINode.ts +142 -36
- package/runtime/sdk/ui/UIPager.ts +29 -28
- package/runtime/sdk/ui/UIPopover.ts +10 -16
- package/runtime/sdk/ui/UIScreen.ts +23 -40
- package/runtime/sdk/ui/UIScrollable.ts +37 -47
- package/runtime/sdk/ui/UISpacer.ts +4 -8
- package/runtime/sdk/ui/UITabs.ts +175 -0
- package/runtime/sdk/ui/UIText.ts +7 -13
- package/runtime/sdk/ui/UIVideo.ts +11 -13
- package/runtime/sdk/ui/UIVirtualizedList.ts +43 -49
- package/runtime/sdk/ui/UIWidget.ts +24 -36
- package/runtime/sdk/ui/fonts.ts +3 -0
- package/runtime/sdk/ui/router.ts +13 -0
- package/runtime/sdk/ui/theme.ts +21 -30
- package/runtime/sdk-types.json +1 -1
|
@@ -1,46 +1,38 @@
|
|
|
1
1
|
import type { UIContainerStyle } from "./UIContainer"
|
|
2
|
-
import { type
|
|
2
|
+
import { type UIChildArg, type UIContainerBase, type UINodeChild, type ChildrenFn, type Color, ContainerElement, buildUI } from "./UINode"
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
82
|
-
export function UIScrollable(style: UIScrollableStyle, children
|
|
83
|
-
export function UIScrollable(...args: []
|
|
84
|
-
|
|
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
|
|
1
|
+
import { Element, type ElementStyle, type UIElementBase } from "./UINode"
|
|
2
2
|
|
|
3
3
|
export type UISpacerStyle = ElementStyle
|
|
4
4
|
|
|
5
|
-
|
|
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
|
+
}
|
package/runtime/sdk/ui/UIText.ts
CHANGED
|
@@ -1,22 +1,13 @@
|
|
|
1
1
|
import { createBinding } from "../core/signals"
|
|
2
|
-
import { Element, type
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
*
|
|
19
|
-
*
|
|
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
|
|
21
|
+
/** Extra px around the viewport kept mounted. Default: one viewport height. */
|
|
24
22
|
overscan?: number
|
|
25
|
-
/**
|
|
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
|
-
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
* (
|
|
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
|
-
/**
|
|
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
|
-
// ---
|
|
69
|
-
_overscan: number
|
|
71
|
+
// --- read by native side in createUINode (copyProperty / copyNodePtr) ---
|
|
72
|
+
_overscan: number
|
|
70
73
|
_inverted: boolean
|
|
71
|
-
_keys: string[] = []
|
|
72
|
-
_estimates: number[] = []
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
}
|