lecodes-cli 0.11.0 → 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/dist/index.js +48454 -7370
- package/package.json +4 -3
- 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 +16 -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 +6 -12
- package/runtime/sdk/ui/UIBottomSheet.ts +0 -4
- package/runtime/sdk/ui/UIButton.ts +2 -21
- package/runtime/sdk/ui/UIContainer.ts +4 -58
- package/runtime/sdk/ui/UIImage.ts +2 -13
- package/runtime/sdk/ui/UIInput.ts +9 -21
- package/runtime/sdk/ui/UIModal.ts +0 -4
- package/runtime/sdk/ui/UINode.ts +37 -0
- package/runtime/sdk/ui/UIPager.ts +2 -13
- package/runtime/sdk/ui/UIScreen.ts +4 -20
- package/runtime/sdk/ui/UIScrollable.ts +2 -20
- package/runtime/sdk/ui/UISpacer.ts +2 -8
- package/runtime/sdk/ui/UIText.ts +2 -13
- package/runtime/sdk/ui/UIVideo.ts +6 -13
- package/runtime/sdk/ui/UIVirtualizedList.ts +10 -10
- package/runtime/sdk/ui/UIWidget.ts +3 -19
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
// The scene-file literal grammar markers, shared by `.scene.ts` (defineScene, 3D) and
|
|
2
|
+
// `.scene2d.ts` (defineScene2d, 2D): `use(Ctor, props)` aspect entries, `ref('name')` node
|
|
3
|
+
// references, `make(fn, args)` code-built sources — plus the ref-resolution helpers both loaders
|
|
4
|
+
// run. One implementation so the two formats can't drift; the engine-specific node/def types stay
|
|
5
|
+
// in their own modules (defineScene.ts / defineScene2d.ts), which re-export these for user code.
|
|
6
|
+
|
|
7
|
+
import type { Aspect, AspectCtor } from "../core/Aspect"
|
|
8
|
+
import type { Node } from "../gl/Node"
|
|
9
|
+
|
|
10
|
+
/** One aspect to attach, created by `use(Ctor, props)`. */
|
|
11
|
+
export type AspectEntry<A extends Aspect<any, any> = Aspect<any, any>> = {
|
|
12
|
+
/** @internal grammar marker. */
|
|
13
|
+
readonly __use: true
|
|
14
|
+
ctor: AspectCtor<A>
|
|
15
|
+
props?: Partial<A>
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Reference an aspect in a scene file: `aspects: [use(Physics, { motion: 'static' })]`. Props are
|
|
19
|
+
* typechecked against the aspect's fields, exactly like `node.aspect(Ctor, props)`. */
|
|
20
|
+
export const use = <A extends Aspect<any, any>>(ctor: AspectCtor<A>, props?: Partial<A>): AspectEntry<A> =>
|
|
21
|
+
({ __use: true, ctor, props })
|
|
22
|
+
|
|
23
|
+
/** A `make(fn, args)` source entry — see {@link make}. `N` is the node family the factory returns
|
|
24
|
+
* (3D `Node` by default; a `.scene2d.ts` file's factories return `Node2D`). */
|
|
25
|
+
export type MakeEntry<A extends Record<string, unknown> = Record<string, unknown>, N = Node> = {
|
|
26
|
+
/** @internal grammar marker. */
|
|
27
|
+
readonly __make: true
|
|
28
|
+
fn: (args: A) => N | Promise<N>
|
|
29
|
+
args?: A
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* A code-created source in a scene file: `tower: { make: make(buildTower, { floors: 5 }), … }`.
|
|
34
|
+
* The factory runs after every scene node exists (so `ref()` args resolve, forward references
|
|
35
|
+
* included) and its returned subtree mounts under the def node — the def's transform stays
|
|
36
|
+
* editor-owned, so moving the node never re-calls the factory. Args must be literal data (same
|
|
37
|
+
* grammar as aspect props); in the editor they edit as fields, and an arg change re-CALLS the
|
|
38
|
+
* factory live — the code is already in the bundle, so no compile happens. Keep factories pure
|
|
39
|
+
* builders: same args → same subtree, no side effects outside the returned nodes.
|
|
40
|
+
*/
|
|
41
|
+
export const make = <A extends Record<string, unknown>, N>(
|
|
42
|
+
fn: (args: A) => N | Promise<N>, args?: A,
|
|
43
|
+
): MakeEntry<A, N> => ({ __make: true, fn, args })
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Reference another scene node by name in an aspect's props:
|
|
47
|
+
* `use(Road, { from: ref('pointA'), to: ref('pointB') })`. Resolves to the live node when aspects
|
|
48
|
+
* attach — after EVERY node of the scene exists, so forward references work. An unknown name
|
|
49
|
+
* resolves to `null` (type your aspect field `Node | null`). Scene files only — hand-written code
|
|
50
|
+
* passes nodes directly: `node.aspect(Road, { from: nodes.pointA })`.
|
|
51
|
+
*/
|
|
52
|
+
export const ref = <T = Node>(name: string): T => ({ $ref: name } as unknown as T)
|
|
53
|
+
|
|
54
|
+
export const isNodeRef = (v: unknown): v is { $ref: string } =>
|
|
55
|
+
typeof v === "object" && v !== null && typeof (v as { $ref?: unknown }).$ref === "string"
|
|
56
|
+
|
|
57
|
+
/** Swap `ref()` markers in aspect props for the live nodes (top level + one array level deep). */
|
|
58
|
+
export const resolveRefs = <N>(
|
|
59
|
+
props: Record<string, unknown> | undefined, nodes: Record<string, N>,
|
|
60
|
+
): Record<string, unknown> | undefined => {
|
|
61
|
+
if (!props) return props
|
|
62
|
+
let out: Record<string, unknown> | undefined
|
|
63
|
+
for (const [ k, v ] of Object.entries(props)) {
|
|
64
|
+
if (isNodeRef(v)) {
|
|
65
|
+
;(out ??= { ...props })[k] = nodes[v.$ref] ?? null
|
|
66
|
+
} else if (Array.isArray(v) && v.some(isNodeRef)) {
|
|
67
|
+
;(out ??= { ...props })[k] = v.map((el) => (isNodeRef(el) ? nodes[el.$ref] ?? null : el))
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return out ?? props
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Names of the nodes a props record's ref() values point at (editor dep tracking). */
|
|
74
|
+
export const collectRefDeps = (props: Record<string, unknown>): Set<string> => {
|
|
75
|
+
const deps = new Set<string>()
|
|
76
|
+
for (const v of Object.values(props)) {
|
|
77
|
+
if (isNodeRef(v)) deps.add(v.$ref)
|
|
78
|
+
else if (Array.isArray(v)) for (const el of v) if (isNodeRef(el)) deps.add(el.$ref)
|
|
79
|
+
}
|
|
80
|
+
return deps
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** The edit-mode flag an editor host sets before running the bundle (shared by both formats). */
|
|
84
|
+
export const EDIT_FLAG = "__lecodesSceneEdit"
|
|
85
|
+
export const isEditMode = (): boolean => (globalThis as Record<string, unknown>)[EDIT_FLAG] === true
|
|
@@ -11,29 +11,30 @@
|
|
|
11
11
|
|
|
12
12
|
import { _channelCall, _channelEmit, _channelOff, _channelOn, type ChannelListeners } from "../runtime/channel"
|
|
13
13
|
import { _bumpNavEpoch, _navSupported, _setCurrent, Presentable, type PresentOptions } from "./presentable"
|
|
14
|
-
import { Element, type
|
|
14
|
+
import { Element, type BaseStyle, type DrawableStyle, type ElementStyle, type UIElementBase } from "./UINode"
|
|
15
15
|
|
|
16
16
|
export type UINativeViewStyle = ElementStyle & DrawableStyle
|
|
17
17
|
|
|
18
18
|
/** An instance of a host-registered platform view (map, QR scanner, camera preview, …) —
|
|
19
19
|
* embeddable among a screen's children AND fullscreen-presentable (`open()`); one instance lives
|
|
20
20
|
* in one place at a time. Talk to it via the `call`/`on` channel. */
|
|
21
|
-
export interface NativeView {
|
|
21
|
+
export interface NativeView extends UIElementBase<UINativeViewStyle, DrawableStyle & BaseStyle> {
|
|
22
22
|
readonly type: "native",
|
|
23
23
|
/** The registered view kind this instance resolves to (the `registerView` name). */
|
|
24
24
|
readonly viewName: string,
|
|
25
25
|
/** Creation params, passed to the host factory (JSON-serializable). */
|
|
26
26
|
readonly params: any,
|
|
27
|
-
style: Style<this, UINativeViewStyle>,
|
|
28
|
-
animateTo: AnimateStyle<this, DrawableStyle & BaseStyle>
|
|
29
|
-
animateFrom: AnimateStyle<this, DrawableStyle & BaseStyle>
|
|
30
27
|
|
|
31
28
|
/** Show fullscreen as the current destination (see Presentable). */
|
|
32
29
|
open(options?: PresentOptions): void,
|
|
30
|
+
/** Dismiss if this is the visible destination. */
|
|
33
31
|
close(): void,
|
|
34
32
|
|
|
33
|
+
/** Became the visible destination. */
|
|
35
34
|
onOpen(callback: () => void): this
|
|
35
|
+
/** Stopped being the visible destination. */
|
|
36
36
|
onClose(callback: () => void): this
|
|
37
|
+
/** Hardware/system back while current. */
|
|
37
38
|
onBackPressed(callback: () => void): this
|
|
38
39
|
|
|
39
40
|
/** Invoke a method on the native view (`map.call("setCenter", [lng, lat])`). Args and the
|
|
@@ -42,13 +43,6 @@ export interface NativeView {
|
|
|
42
43
|
/** Subscribe to an event the native view emits (`map.on("markerTap", cb)`). */
|
|
43
44
|
on(event: string, callback: (data?: any) => void): this
|
|
44
45
|
off(event: string, callback: (data?: any) => void): this
|
|
45
|
-
|
|
46
|
-
onLayout(onLayout: OnLayoutCallback): this
|
|
47
|
-
getBoundingClientRect(): BoundingClientRect | null
|
|
48
|
-
/** Style-class proxy — read `el.class.checked`, set `el.class.checked = true`, toggle with
|
|
49
|
-
* `!el.class.checked`, bind `el.class.done = () => sig.value`, batch/chain `el.class({ … })`.
|
|
50
|
-
* Classes cascade to descendants. See {@link Classes}. */
|
|
51
|
-
readonly class: Classes<this>
|
|
52
46
|
}
|
|
53
47
|
|
|
54
48
|
let nextViewId = 1
|
|
@@ -51,10 +51,6 @@ export interface UIBottomSheet extends UIWidget {
|
|
|
51
51
|
/** `dismissible(false)`: scrim taps, the back button and dragging below the lowest detent no
|
|
52
52
|
* longer close the sheet (a drag collapses to the lowest detent instead). Default `true`. */
|
|
53
53
|
dismissible(enabled: boolean): this,
|
|
54
|
-
|
|
55
|
-
append(...nodes: UINodeChild[]): this
|
|
56
|
-
insert(index: number, ...nodes: UINodeChild[]): this
|
|
57
|
-
remove(...nodes: UINodeChild[]): this
|
|
58
54
|
}
|
|
59
55
|
|
|
60
56
|
export class BottomSheetElement extends ModalElement {
|
|
@@ -1,18 +1,14 @@
|
|
|
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,
|
|
3
|
+
import { type ElementStyle, type ContainerStyle, ContainerElement, buildUI, type DrawableStyle, type UIChildArg, type UIContainerBase, type UINodeChild, type ChildrenFn, type BaseStyle, type Color } from "./UINode"
|
|
4
4
|
|
|
5
5
|
export type UIButtonStyle = ElementStyle & DrawableStyle & ContainerStyle & { rippleColor?: Color | "default" }
|
|
6
6
|
|
|
7
7
|
/** A tappable container — row + centered children by default. Press feedback via the `onPressed`
|
|
8
8
|
* style block, the `$pressed` class, or `rippleColor`. */
|
|
9
|
-
export interface UIButton {
|
|
9
|
+
export interface UIButton extends UIContainerBase<UIButtonStyle & { onPressed?: UIButtonStyle & { duration?: number } }, DrawableStyle & BaseStyle> {
|
|
10
10
|
readonly type: "button",
|
|
11
11
|
|
|
12
|
-
style: Style<this, UIButtonStyle & { onPressed?: UIButtonStyle & { duration?: number } }>,
|
|
13
|
-
animateTo: AnimateStyle<this, DrawableStyle & BaseStyle>,
|
|
14
|
-
animateFrom: AnimateStyle<this, DrawableStyle & BaseStyle>
|
|
15
|
-
|
|
16
12
|
/** Fired on a completed tap/click. */
|
|
17
13
|
onClick(callback: (ev: ClickEventType<UIButton>) => void): this,
|
|
18
14
|
/** Fired when a touch begins; `ev.track(...)` takes over the rest of the gesture (dragging). */
|
|
@@ -23,21 +19,6 @@ export interface UIButton {
|
|
|
23
19
|
onLongPress(callback: (ev: LongPressEventType<UIButton>) => void): this,
|
|
24
20
|
/** Is a finger currently down on the button? */
|
|
25
21
|
isPressed(): boolean,
|
|
26
|
-
|
|
27
|
-
append(...nodes: UINodeChild[]): this
|
|
28
|
-
insert(index: number, ...nodes: UINodeChild[]): this
|
|
29
|
-
remove(...nodes: UINodeChild[]): this
|
|
30
|
-
setContent(nodes: UINodeChild[] | ChildrenFn): this
|
|
31
|
-
|
|
32
|
-
onLayout(onLayout: OnLayoutCallback): this
|
|
33
|
-
getBoundingClientRect(): BoundingClientRect | null
|
|
34
|
-
|
|
35
|
-
/** Style-class proxy — read `el.class.checked`, set `el.class.checked = true`, toggle with
|
|
36
|
-
* `!el.class.checked`, bind `el.class.done = () => sig.value`, batch/chain `el.class({ … })`.
|
|
37
|
-
* Classes cascade to descendants. See {@link Classes}. */
|
|
38
|
-
readonly class: Classes<this>
|
|
39
|
-
|
|
40
|
-
readonly children: UINodeChild[],
|
|
41
22
|
}
|
|
42
23
|
|
|
43
24
|
|
|
@@ -1,28 +1,10 @@
|
|
|
1
|
-
import { ContainerElement, buildUI, type
|
|
1
|
+
import { ContainerElement, buildUI, type ChildrenFn, type ContainerStyle, type DrawableStyle, type ElementStyle, type UIChildArg, type UIContainerBase, type UINodeChild } from "./UINode"
|
|
2
2
|
|
|
3
3
|
export type UIContainerStyle = ElementStyle & DrawableStyle & ContainerStyle
|
|
4
4
|
|
|
5
5
|
/** A vertical flex container — children stack top to bottom. */
|
|
6
|
-
export interface UIColumn {
|
|
6
|
+
export interface UIColumn extends UIContainerBase<UIContainerStyle> {
|
|
7
7
|
readonly type: "column",
|
|
8
|
-
style: Style<this, UIContainerStyle>,
|
|
9
|
-
animateTo: AnimateStyle<this, UIContainerStyle>
|
|
10
|
-
animateFrom: AnimateStyle<this, UIContainerStyle>
|
|
11
|
-
|
|
12
|
-
append(...nodes: UINodeChild[]): UIColumn
|
|
13
|
-
insert(index: number, ...nodes: UINodeChild[]): UIColumn
|
|
14
|
-
remove(...nodes: UINodeChild[]): UIColumn
|
|
15
|
-
setContent(nodes: UINodeChild[] | ChildrenFn): this
|
|
16
|
-
|
|
17
|
-
onLayout(onLayout: OnLayoutCallback): this
|
|
18
|
-
getBoundingClientRect(): BoundingClientRect | null
|
|
19
|
-
|
|
20
|
-
/** Style-class proxy — read `el.class.checked`, set `el.class.checked = true`, toggle with
|
|
21
|
-
* `!el.class.checked`, bind `el.class.done = () => sig.value`, batch/chain `el.class({ … })`.
|
|
22
|
-
* Classes cascade to descendants. See {@link Classes}. */
|
|
23
|
-
readonly class: Classes<this>
|
|
24
|
-
|
|
25
|
-
readonly children: UINodeChild[]
|
|
26
8
|
}
|
|
27
9
|
const makeColumn = (style: any, children: any) => new ContainerElement("column", style, children)
|
|
28
10
|
|
|
@@ -44,26 +26,8 @@ export function UIColumn(...args: any[]): UIColumn {
|
|
|
44
26
|
}
|
|
45
27
|
|
|
46
28
|
/** A horizontal flex container — children flow left to right. */
|
|
47
|
-
export interface UIRow {
|
|
29
|
+
export interface UIRow extends UIContainerBase<UIContainerStyle> {
|
|
48
30
|
readonly type: "row",
|
|
49
|
-
style: Style<this, UIContainerStyle>,
|
|
50
|
-
animateTo: AnimateStyle<this, UIContainerStyle>,
|
|
51
|
-
animateFrom: AnimateStyle<this, UIContainerStyle>,
|
|
52
|
-
|
|
53
|
-
append(...nodes: UINodeChild[]): UIRow
|
|
54
|
-
insert(index: number, ...nodes: UINodeChild[]): UIRow
|
|
55
|
-
remove(...nodes: UINodeChild[]): UIRow
|
|
56
|
-
setContent(nodes: UINodeChild[] | ChildrenFn): this
|
|
57
|
-
|
|
58
|
-
onLayout(onLayout: OnLayoutCallback): this
|
|
59
|
-
getBoundingClientRect(): BoundingClientRect | null
|
|
60
|
-
|
|
61
|
-
/** Style-class proxy — read `el.class.checked`, set `el.class.checked = true`, toggle with
|
|
62
|
-
* `!el.class.checked`, bind `el.class.done = () => sig.value`, batch/chain `el.class({ … })`.
|
|
63
|
-
* Classes cascade to descendants. See {@link Classes}. */
|
|
64
|
-
readonly class: Classes<this>
|
|
65
|
-
|
|
66
|
-
readonly children: UINodeChild[],
|
|
67
31
|
}
|
|
68
32
|
const ROW_DEFAULTS = { flexDirection: "row" } as const
|
|
69
33
|
const makeRow = (style: any, children: any) => new ContainerElement("row", style, children)
|
|
@@ -78,26 +42,8 @@ export function UIRow(...args: any[]): UIRow {
|
|
|
78
42
|
}
|
|
79
43
|
|
|
80
44
|
/** A centering container — `justifyContent`/`alignItems` default to center. */
|
|
81
|
-
export interface UIBox {
|
|
45
|
+
export interface UIBox extends UIContainerBase<UIContainerStyle> {
|
|
82
46
|
readonly type: "box",
|
|
83
|
-
style: Style<this, UIContainerStyle>,
|
|
84
|
-
animateTo: AnimateStyle<this, UIContainerStyle>,
|
|
85
|
-
animateFrom: AnimateStyle<this, UIContainerStyle>,
|
|
86
|
-
|
|
87
|
-
append(...nodes: UINodeChild[]): this
|
|
88
|
-
insert(index: number, ...nodes: UINodeChild[]): this
|
|
89
|
-
remove(...nodes: UINodeChild[]): this
|
|
90
|
-
setContent(nodes: UINodeChild[] | ChildrenFn): this
|
|
91
|
-
|
|
92
|
-
onLayout(onLayout: OnLayoutCallback): this
|
|
93
|
-
getBoundingClientRect(): BoundingClientRect | null
|
|
94
|
-
|
|
95
|
-
/** Style-class proxy — read `el.class.checked`, set `el.class.checked = true`, toggle with
|
|
96
|
-
* `!el.class.checked`, bind `el.class.done = () => sig.value`, batch/chain `el.class({ … })`.
|
|
97
|
-
* Classes cascade to descendants. See {@link Classes}. */
|
|
98
|
-
readonly class: Classes<this>
|
|
99
|
-
|
|
100
|
-
readonly children: UINodeChild[],
|
|
101
47
|
}
|
|
102
48
|
const BOX_DEFAULTS = { justifyContent: "center", alignItems: "center" } as const
|
|
103
49
|
|
|
@@ -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
|
|
5
|
+
import { Element, type Color, type ElementStyle, type UIElementBase } from "./UINode"
|
|
6
6
|
|
|
7
7
|
export type UIImageStyle = ElementStyle & { borderRadius?: number, objectFit?: "cover" | "contain" | "fill", tintColor?: Color }
|
|
8
8
|
|
|
@@ -23,27 +23,16 @@ export type ResolvedImageSource = string | FetchResponse | File | SvgSource | Ca
|
|
|
23
23
|
|
|
24
24
|
/** An image element. Sources: URL / `asset(...)`, fetch response / File, an SVG source
|
|
25
25
|
* (`assetIcon`), or a live Canvas (`canvas.update()` refreshes the node in place). */
|
|
26
|
-
export interface UIImage {
|
|
26
|
+
export interface UIImage extends UIElementBase<UIImageStyle> {
|
|
27
27
|
readonly type: "image",
|
|
28
28
|
/** The image source — writable to swap the picture in place. */
|
|
29
29
|
get src(): ResolvedImageSource
|
|
30
30
|
set src(value: ImageSource)
|
|
31
|
-
style: Style<this, UIImageStyle>
|
|
32
|
-
animateTo: AnimateStyle<this, UIImageStyle>
|
|
33
|
-
animateFrom: AnimateStyle<this, UIImageStyle>
|
|
34
31
|
|
|
35
32
|
/** Render only a sub-rectangle of the source texture (texture pixels) — the atlas/spritesheet
|
|
36
33
|
* primitive: crop once, then swap the rect per frame for sprite animation. The rect's w/h
|
|
37
34
|
* become the node's intrinsic size and override `objectFit` — the cropped frame fills the box. */
|
|
38
35
|
setSourceRect(x: number, y: number, w: number, h: number): this
|
|
39
|
-
|
|
40
|
-
onLayout(onLayout: OnLayoutCallback): this
|
|
41
|
-
getBoundingClientRect(): BoundingClientRect | null
|
|
42
|
-
|
|
43
|
-
/** Style-class proxy — read `el.class.checked`, set `el.class.checked = true`, toggle with
|
|
44
|
-
* `!el.class.checked`, bind `el.class.done = () => sig.value`, batch/chain `el.class({ … })`.
|
|
45
|
-
* Classes cascade to descendants. See {@link Classes}. */
|
|
46
|
-
readonly class: Classes<this>
|
|
47
36
|
}
|
|
48
37
|
|
|
49
38
|
class ImageElement extends Element<"image"> {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type ElementStyle, type DrawableStyle,
|
|
1
|
+
import { type ElementStyle, type DrawableStyle, Element, type TextStyle, type Color, type BaseStyle, type UIElementBase } from "./UINode";
|
|
2
2
|
|
|
3
3
|
type InputStyle = {
|
|
4
4
|
placeholder?: string,
|
|
@@ -51,7 +51,7 @@ type SubmitStyle = {
|
|
|
51
51
|
|
|
52
52
|
/** A single-line text input. Keyboard/picker kind via the `type` style, return-key behavior via
|
|
53
53
|
* `enterKey` + `onSubmit`; keyboard policy on `keyboardShrink`/`keyboardDismiss`. */
|
|
54
|
-
export interface UIInput {
|
|
54
|
+
export interface UIInput extends UIElementBase<UIInputStyle & SubmitStyle & { onFocused?: UIInputStyle & { duration?: number } }, DrawableStyle & BaseStyle> {
|
|
55
55
|
readonly type: "input",
|
|
56
56
|
|
|
57
57
|
/**
|
|
@@ -62,51 +62,39 @@ export interface UIInput {
|
|
|
62
62
|
*/
|
|
63
63
|
value: string
|
|
64
64
|
|
|
65
|
-
|
|
66
|
-
animateTo: AnimateStyle<this, DrawableStyle & BaseStyle>
|
|
67
|
-
animateFrom: AnimateStyle<this, DrawableStyle & BaseStyle>
|
|
68
|
-
|
|
65
|
+
/** The text changed — fires on every user edit. */
|
|
69
66
|
onChange(callback: (value: string) => void): UIInput
|
|
67
|
+
/** Gained focus (keyboard opens). */
|
|
70
68
|
onFocus(callback: () => void): UIInput
|
|
69
|
+
/** Lost focus. */
|
|
71
70
|
onBlur(callback: () => void): UIInput
|
|
72
71
|
/** The keyboard's return key was pressed. `value` is the current text. See `enterKey`. */
|
|
73
72
|
onSubmit(callback: (value: string) => void): UIInput
|
|
74
|
-
onLayout(onLayout: OnLayoutCallback): this
|
|
75
|
-
getBoundingClientRect(): BoundingClientRect | null
|
|
76
73
|
|
|
77
74
|
/** Focus the input (opens the keyboard). No-op before mount. */
|
|
78
75
|
focus(): void
|
|
79
76
|
/** Release focus (dismisses the keyboard when this input held it). */
|
|
80
77
|
blur(): void
|
|
81
|
-
|
|
82
|
-
/** Style-class proxy — read `el.class.checked`, set `el.class.checked = true`, toggle with
|
|
83
|
-
* `!el.class.checked`, bind `el.class.done = () => sig.value`, batch/chain `el.class({ … })`.
|
|
84
|
-
* Classes cascade to descendants. See {@link Classes}. */
|
|
85
|
-
readonly class: Classes<this>
|
|
86
78
|
}
|
|
87
79
|
|
|
88
80
|
/** A multi-line text field — see the {@link UITextArea} factory for the auto-grow contract. */
|
|
89
|
-
export interface UITextArea {
|
|
81
|
+
export interface UITextArea extends UIElementBase<UIInputStyle & { onFocused?: UIInputStyle & { duration?: number } }, DrawableStyle & BaseStyle> {
|
|
90
82
|
readonly type: "textarea",
|
|
91
83
|
|
|
92
84
|
/** The field's text — same contract as {@link UIInput.value}: settable before mount to pre-fill. */
|
|
93
85
|
value: string
|
|
94
86
|
|
|
95
|
-
|
|
96
|
-
|
|
87
|
+
/** The text changed — fires on every user edit. */
|
|
97
88
|
onChange(callback: (value: string) => void): UITextArea
|
|
89
|
+
/** Gained focus (keyboard opens). */
|
|
98
90
|
onFocus(callback: () => void): UITextArea
|
|
91
|
+
/** Lost focus. */
|
|
99
92
|
onBlur(callback: () => void): UITextArea
|
|
100
93
|
|
|
101
94
|
/** Focus the text area (opens the keyboard). No-op before mount. */
|
|
102
95
|
focus(): void
|
|
103
96
|
/** Release focus (dismisses the keyboard when this text area held it). */
|
|
104
97
|
blur(): void
|
|
105
|
-
|
|
106
|
-
/** Style-class proxy — read `el.class.checked`, set `el.class.checked = true`, toggle with
|
|
107
|
-
* `!el.class.checked`, bind `el.class.done = () => sig.value`, batch/chain `el.class({ … })`.
|
|
108
|
-
* Classes cascade to descendants. See {@link Classes}. */
|
|
109
|
-
readonly class: Classes<this>
|
|
110
98
|
}
|
|
111
99
|
|
|
112
100
|
class InputElement<T extends "input" | "textarea"> extends Element<T> {
|
|
@@ -50,10 +50,6 @@ export interface UIModal extends UIWidget {
|
|
|
50
50
|
/** `dismissible(false)` keeps scrim taps and the back button from closing the modal
|
|
51
51
|
* (forced-choice dialogs). Default `true`. */
|
|
52
52
|
dismissible(enabled: boolean): this,
|
|
53
|
-
|
|
54
|
-
append(...nodes: UINodeChild[]): this
|
|
55
|
-
insert(index: number, ...nodes: UINodeChild[]): this
|
|
56
|
-
remove(...nodes: UINodeChild[]): this
|
|
57
53
|
}
|
|
58
54
|
|
|
59
55
|
export class ModalElement extends WidgetElement {
|
package/runtime/sdk/ui/UINode.ts
CHANGED
|
@@ -362,6 +362,43 @@ export type ClassValue = boolean | (() => boolean)
|
|
|
362
362
|
* single-key writes are runtime-coerced with `!!`. */
|
|
363
363
|
export type Classes<R> = ((classes: Record<string, ClassValue>) => R) & { [key: string]: any }
|
|
364
364
|
|
|
365
|
+
/** Members every UI element shares — element interfaces extend this so docs and types live in one
|
|
366
|
+
* place. `S` is the element's style object; `A` is the subset `animateTo` accepts (defaults to `S`). */
|
|
367
|
+
export interface UIElementBase<S extends object, A extends object = S> {
|
|
368
|
+
// NB: no `name` member here. Declaring it would give every element a property in common with
|
|
369
|
+
// the style types (BaseStyle.name), letting TS's weak-type check match a NODE against a legacy
|
|
370
|
+
// style-first overload — `UIPager(UIText("x"))` must stay a type error (tests/ui-types.test.ts).
|
|
371
|
+
/** Style: `.style({...})` merges (chainable); `el.style.key = v` writes one prop, a `() => v`
|
|
372
|
+
* value binds it to signals — see {@link Style}. */
|
|
373
|
+
style: Style<this, S>
|
|
374
|
+
/** Tween to the target style — meta keys `duration`/`delay`/`loop`/…, see {@link AnimateStyle}. */
|
|
375
|
+
animateTo: AnimateStyle<this, A>
|
|
376
|
+
/** Tween from the given style to the current one (entrance animations). */
|
|
377
|
+
animateFrom: AnimateStyle<this, A>
|
|
378
|
+
/** Fires after every layout pass with the parent-relative box. */
|
|
379
|
+
onLayout(onLayout: OnLayoutCallback): this
|
|
380
|
+
/** Absolute rect in device space, read live (includes scroll); `null` before mount. */
|
|
381
|
+
getBoundingClientRect(): BoundingClientRect | null
|
|
382
|
+
/** Style-class proxy — read `el.class.checked`, set `el.class.checked = true`, toggle with
|
|
383
|
+
* `!el.class.checked`, bind `el.class.done = () => sig.value`, batch/chain `el.class({ … })`.
|
|
384
|
+
* Classes cascade to descendants. See {@link Classes}. */
|
|
385
|
+
readonly class: Classes<this>
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/** {@link UIElementBase} plus the child-management surface every container shares. */
|
|
389
|
+
export interface UIContainerBase<S extends object, A extends object = S> extends UIElementBase<S, A> {
|
|
390
|
+
/** Append children. */
|
|
391
|
+
append(...nodes: UINodeChild[]): this
|
|
392
|
+
/** Insert children at `index`. */
|
|
393
|
+
insert(index: number, ...nodes: UINodeChild[]): this
|
|
394
|
+
/** Remove (unmount) the given children. */
|
|
395
|
+
remove(...nodes: UINodeChild[]): this
|
|
396
|
+
/** Replace all children — an array, or a function for reactive children. */
|
|
397
|
+
setContent(nodes: UINodeChild[] | ChildrenFn): this
|
|
398
|
+
/** The children array — mutate via `append`/`insert`/`remove`/`setContent`. */
|
|
399
|
+
readonly children: UINodeChild[]
|
|
400
|
+
}
|
|
401
|
+
|
|
365
402
|
/** Orientation state blocks — override props applied only in landscape / portrait (like a
|
|
366
403
|
* `$`-class, keyed by device orientation instead of `el.class`). Same full-`T` rule as class
|
|
367
404
|
* blocks. */
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ContainerElement, type
|
|
1
|
+
import { ContainerElement, type BaseStyle, type ChildrenFn, type DrawableStyle, type ElementStyle, type UIElementBase, type UINodeChild } from "./UINode"
|
|
2
2
|
import { disposeBinding } from "../core/signals"
|
|
3
3
|
import type { UIScreen } from "./UIScreen"
|
|
4
4
|
|
|
@@ -41,11 +41,8 @@ const _activeStack: PagerElement[] = []
|
|
|
41
41
|
*
|
|
42
42
|
* To open something over *everything* (above bars, above the pager), use `Router.push` instead.
|
|
43
43
|
*/
|
|
44
|
-
export interface UIPager {
|
|
44
|
+
export interface UIPager extends UIElementBase<UIPagerStyle, DrawableStyle & BaseStyle> {
|
|
45
45
|
readonly type: "pager",
|
|
46
|
-
style: Style<this, UIPagerStyle>,
|
|
47
|
-
animateTo: AnimateStyle<this, DrawableStyle & BaseStyle>,
|
|
48
|
-
animateFrom: AnimateStyle<this, DrawableStyle & BaseStyle>,
|
|
49
46
|
|
|
50
47
|
/** Replace the pager's tabs — one screen per tab, in order. Every tab becomes a fresh
|
|
51
48
|
* one-screen stack (pushed screens are dropped) and the selection is clamped into range;
|
|
@@ -80,14 +77,6 @@ export interface UIPager {
|
|
|
80
77
|
readonly depth: number,
|
|
81
78
|
/** Fires when the current tab's depth changes — push, pop, replace, or a back-swipe. */
|
|
82
79
|
onChange(callback: (depth: number) => void): this,
|
|
83
|
-
|
|
84
|
-
onLayout(onLayout: OnLayoutCallback): this,
|
|
85
|
-
getBoundingClientRect(): BoundingClientRect | null,
|
|
86
|
-
|
|
87
|
-
/** Style-class proxy — read `el.class.checked`, set `el.class.checked = true`, toggle with
|
|
88
|
-
* `!el.class.checked`, bind `el.class.done = () => sig.value`, batch/chain `el.class({ … })`.
|
|
89
|
-
* Classes cascade to descendants. See {@link Classes}. */
|
|
90
|
-
readonly class: Classes<this>,
|
|
91
80
|
}
|
|
92
81
|
|
|
93
82
|
export class PagerElement extends ContainerElement<"pager"> {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { TouchStartEvent } from "../runtime/touch"
|
|
2
2
|
import { _bumpNavEpoch, _navSupported, _setCurrent, Presentable, type PresentOptions } from "./presentable"
|
|
3
|
-
import { ContainerElement, buildUI, type
|
|
3
|
+
import { ContainerElement, buildUI, type BaseStyle, type ContainerStyle, type DrawableStyle, type PaddingStyle, type UIChildArg, type UIContainerBase, type UINodeChild, type ChildrenFn, } from "./UINode"
|
|
4
4
|
|
|
5
5
|
export type UIScreenStyle = ContainerStyle & DrawableStyle & PaddingStyle & BaseStyle
|
|
6
6
|
|
|
@@ -8,24 +8,15 @@ export type UIScreenStyle = ContainerStyle & DrawableStyle & PaddingStyle & Base
|
|
|
8
8
|
* fixed chrome (header, tab bar) plus, when the content overflows, one `UIScrollable` body child
|
|
9
9
|
* (`flexGrow: 1`). Scroll events and pull-to-refresh live on the scroll containers
|
|
10
10
|
* (`UIScrollable`, `UIVirtualizedList`). */
|
|
11
|
-
export interface UIScreen {
|
|
11
|
+
export interface UIScreen extends UIContainerBase<UIScreenStyle, DrawableStyle & BaseStyle> {
|
|
12
12
|
readonly type: "screen",
|
|
13
|
-
style: Style<this, UIScreenStyle>,
|
|
14
|
-
animateTo: AnimateStyle<this, DrawableStyle & BaseStyle>
|
|
15
|
-
animateFrom: AnimateStyle<this, DrawableStyle & BaseStyle>
|
|
16
13
|
|
|
17
14
|
/** Show this screen as the current destination (replaces whatever is visible — a screen, a
|
|
18
15
|
* scene, a native view — suspending an active Router until `Router.restore()`). */
|
|
19
16
|
open(options?: PresentOptions): void,
|
|
17
|
+
/** Dismiss if this screen is the visible destination. */
|
|
20
18
|
close(): void
|
|
21
19
|
|
|
22
|
-
append(...nodes: UINodeChild[]): this
|
|
23
|
-
insert(index: number, ...nodes: UINodeChild[]): this
|
|
24
|
-
remove(...nodes: UINodeChild[]): this
|
|
25
|
-
setContent(nodes: UINodeChild[] | ChildrenFn): this
|
|
26
|
-
|
|
27
|
-
readonly children: UINodeChild[],
|
|
28
|
-
|
|
29
20
|
/** Hardware/system back while this screen is current. */
|
|
30
21
|
onBackPressed(callback: () => void): this
|
|
31
22
|
|
|
@@ -45,14 +36,7 @@ export interface UIScreen {
|
|
|
45
36
|
/** Free a `keepAlive()` screen's retained tree now. No-op unless it's currently detached-and-kept. */
|
|
46
37
|
dispose(): this
|
|
47
38
|
|
|
48
|
-
|
|
49
|
-
getBoundingClientRect(): BoundingClientRect | null
|
|
50
|
-
|
|
51
|
-
/** Style-class proxy — read `el.class.checked`, set `el.class.checked = true`, toggle with
|
|
52
|
-
* `!el.class.checked`, bind `el.class.done = () => sig.value`, batch/chain `el.class({ … })`.
|
|
53
|
-
* Classes cascade to descendants. See {@link Classes}. */
|
|
54
|
-
readonly class: Classes<this>
|
|
55
|
-
|
|
39
|
+
/** Touch began on the screen; `ev.track(...)` takes over the rest of the gesture. */
|
|
56
40
|
onTouchStart(callback: (ev: TouchStartEvent<UIScreen>) => void): this,
|
|
57
41
|
}
|
|
58
42
|
|
|
@@ -1,5 +1,5 @@
|
|
|
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
4
|
export type ScrollStyle = {
|
|
5
5
|
showScrollbar?: boolean,
|
|
@@ -21,24 +21,8 @@ export type UIScrollableStyle = UIContainerStyle & ScrollStyle & { scrollDirecti
|
|
|
21
21
|
|
|
22
22
|
/** A scroll container — vertical by default, `scrollDirection: "horizontal"` for carousels/chip
|
|
23
23
|
* rows. The scrolling body of a screen (screens themselves never scroll). */
|
|
24
|
-
export interface UIScrollable {
|
|
24
|
+
export interface UIScrollable extends UIContainerBase<UIScrollableStyle> {
|
|
25
25
|
readonly type: "scrollable",
|
|
26
|
-
style: Style<this, UIScrollableStyle>,
|
|
27
|
-
animateTo: AnimateStyle<this, UIScrollableStyle>,
|
|
28
|
-
animateFrom: AnimateStyle<this, UIScrollableStyle>,
|
|
29
|
-
|
|
30
|
-
append(...nodes: UINodeChild[]): UIScrollable
|
|
31
|
-
insert(index: number, ...nodes: UINodeChild[]): UIScrollable
|
|
32
|
-
remove(...nodes: UINodeChild[]): UIScrollable
|
|
33
|
-
setContent(nodes: UINodeChild[] | ChildrenFn): this
|
|
34
|
-
|
|
35
|
-
onLayout(onLayout: OnLayoutCallback): this
|
|
36
|
-
getBoundingClientRect(): BoundingClientRect | null
|
|
37
|
-
|
|
38
|
-
/** Style-class proxy — read `el.class.checked`, set `el.class.checked = true`, toggle with
|
|
39
|
-
* `!el.class.checked`, bind `el.class.done = () => sig.value`, batch/chain `el.class({ … })`.
|
|
40
|
-
* Classes cascade to descendants. See {@link Classes}. */
|
|
41
|
-
readonly class: Classes<this>
|
|
42
26
|
|
|
43
27
|
/** Scroll position changed (px from the start edge). */
|
|
44
28
|
onScroll(callback: (scrollPosition: number) => void): UIScrollable,
|
|
@@ -49,8 +33,6 @@ export interface UIScrollable {
|
|
|
49
33
|
/** Pull-to-refresh: the spinner stays until the returned promise settles. Attach BEFORE the
|
|
50
34
|
* node mounts (hosts read it at creation). Vertical only; native hosts — no-op on web. */
|
|
51
35
|
onRefresh(callback: () => void | Promise<void>): UIScrollable
|
|
52
|
-
|
|
53
|
-
readonly children: UINodeChild[]
|
|
54
36
|
}
|
|
55
37
|
|
|
56
38
|
class ScrollableElement extends ContainerElement<"scrollable"> {
|
|
@@ -1,16 +1,10 @@
|
|
|
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 {
|
|
6
|
+
export interface UISpacer extends UIElementBase<UISpacerStyle> {
|
|
7
7
|
readonly type: "spacer",
|
|
8
|
-
style: Style<this, UISpacerStyle>,
|
|
9
|
-
|
|
10
|
-
/** Style-class proxy — read `el.class.checked`, set `el.class.checked = true`, toggle with
|
|
11
|
-
* `!el.class.checked`, bind `el.class.done = () => sig.value`, batch/chain `el.class({ … })`.
|
|
12
|
-
* Classes cascade to descendants. See {@link Classes}. */
|
|
13
|
-
readonly class: Classes<this>
|
|
14
8
|
}
|
|
15
9
|
/** Create a flexible gap (`flexGrow: 1`) — e.g. between a header's title and its actions. */
|
|
16
10
|
export function UISpacer(): UISpacer
|
package/runtime/sdk/ui/UIText.ts
CHANGED
|
@@ -1,24 +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 {
|
|
7
|
+
export interface UIText extends UIElementBase<UITextStyle, DrawableStyle & BaseStyle & TextStyle> {
|
|
8
8
|
readonly type: "text",
|
|
9
9
|
/** The displayed string — writable at any time. */
|
|
10
10
|
text: string,
|
|
11
|
-
style: Style<this, UITextStyle>,
|
|
12
|
-
animateTo: AnimateStyle<this, DrawableStyle & BaseStyle & TextStyle>
|
|
13
|
-
animateFrom: AnimateStyle<this, DrawableStyle & BaseStyle & TextStyle>
|
|
14
|
-
|
|
15
|
-
onLayout(onLayout: OnLayoutCallback): this
|
|
16
|
-
getBoundingClientRect(): BoundingClientRect | null
|
|
17
|
-
|
|
18
|
-
/** Style-class proxy — read `el.class.checked`, set `el.class.checked = true`, toggle with
|
|
19
|
-
* `!el.class.checked`, bind `el.class.done = () => sig.value`, batch/chain `el.class({ … })`.
|
|
20
|
-
* Classes cascade to descendants. See {@link Classes}. */
|
|
21
|
-
readonly class: Classes<this>
|
|
22
11
|
}
|
|
23
12
|
|
|
24
13
|
export class TextElement extends Element<"text"> {
|