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
|
@@ -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,26 +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
|
+
* embeddable among a screen's children AND fullscreen-presentable (`open()`); one instance lives
|
|
20
|
+
* in one place at a time. Talk to it via the `call`/`on` channel. */
|
|
21
|
+
export interface NativeView extends UIElementBase<UINativeViewStyle, DrawableStyle & BaseStyle> {
|
|
19
22
|
readonly type: "native",
|
|
20
23
|
/** The registered view kind this instance resolves to (the `registerView` name). */
|
|
21
24
|
readonly viewName: string,
|
|
22
25
|
/** Creation params, passed to the host factory (JSON-serializable). */
|
|
23
26
|
readonly params: any,
|
|
24
|
-
style: Style<this, UINativeViewStyle>,
|
|
25
|
-
animateTo: AnimateStyle<this, DrawableStyle & BaseStyle>
|
|
26
|
-
animateFrom: AnimateStyle<this, DrawableStyle & BaseStyle>
|
|
27
27
|
|
|
28
28
|
/** Show fullscreen as the current destination (see Presentable). */
|
|
29
29
|
open(options?: PresentOptions): void,
|
|
30
|
+
/** Dismiss if this is the visible destination. */
|
|
30
31
|
close(): void,
|
|
31
32
|
|
|
33
|
+
/** Became the visible destination. */
|
|
32
34
|
onOpen(callback: () => void): this
|
|
35
|
+
/** Stopped being the visible destination. */
|
|
33
36
|
onClose(callback: () => void): this
|
|
37
|
+
/** Hardware/system back while current. */
|
|
34
38
|
onBackPressed(callback: () => void): this
|
|
35
39
|
|
|
36
40
|
/** Invoke a method on the native view (`map.call("setCenter", [lng, lat])`). Args and the
|
|
@@ -39,13 +43,6 @@ export interface NativeView {
|
|
|
39
43
|
/** Subscribe to an event the native view emits (`map.on("markerTap", cb)`). */
|
|
40
44
|
on(event: string, callback: (data?: any) => void): this
|
|
41
45
|
off(event: string, callback: (data?: any) => void): this
|
|
42
|
-
|
|
43
|
-
onLayout(onLayout: OnLayoutCallback): this
|
|
44
|
-
getBoundingClientRect(): BoundingClientRect | null
|
|
45
|
-
/** Style-class proxy — read `el.class.checked`, set `el.class.checked = true`, toggle with
|
|
46
|
-
* `!el.class.checked`, bind `el.class.done = () => sig.value`, batch/chain `el.class({ … })`.
|
|
47
|
-
* Classes cascade to descendants. See {@link Classes}. */
|
|
48
|
-
readonly class: Classes<this>
|
|
49
46
|
}
|
|
50
47
|
|
|
51
48
|
let nextViewId = 1
|
package/runtime/sdk/ui/UI.ts
CHANGED
|
@@ -17,6 +17,7 @@ export { UIPopover, type UIPopoverStyle, type UIPopoverAnchor } from './UIPopove
|
|
|
17
17
|
export { UIRow, UIColumn, UIBox, type UIContainerStyle } from './UIContainer'
|
|
18
18
|
export { UIScrollable, type UIScrollableStyle } from './UIScrollable'
|
|
19
19
|
export { UIPager, type UIPagerStyle } from './UIPager'
|
|
20
|
+
export { UITabs, type UITabDef } from './UITabs'
|
|
20
21
|
|
|
21
22
|
export { UIVirtualizedList } from './UIVirtualizedList'
|
|
22
23
|
|
|
@@ -28,4 +29,11 @@ export type { PresentOptions, Transition, TransitionName, TransitionSpec, Transi
|
|
|
28
29
|
|
|
29
30
|
// Internal: memoized map for reactive children — the chisel `reactiveUi` pass rewrites
|
|
30
31
|
// `list.map(fn)` inside children bindings to `__uiMap(list, fn, slot)` calls.
|
|
31
|
-
export { __uiMap } from './UINode'
|
|
32
|
+
export { __uiMap } from './UINode'
|
|
33
|
+
// Compiler fast paths (chisel `flatten_ui` raw lowering): dispatch-free construction for calls
|
|
34
|
+
// whose arguments the compiler proved to be plain children. Never written by hand.
|
|
35
|
+
export { __UIColumn, __UIRow, __UIBox } from './UIContainer'
|
|
36
|
+
export { __UIButton } from './UIButton'
|
|
37
|
+
export { __UIScreen } from './UIScreen'
|
|
38
|
+
export { __UIScrollable } from './UIScrollable'
|
|
39
|
+
export { __UIWidget } from './UIWidget'
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { ModalElement, type UIModalStyle } from "./UIModal"
|
|
2
2
|
import type { UIWidget } from "./UIWidget"
|
|
3
|
-
import type
|
|
3
|
+
import { buildUI, type ChildrenFn, type UIChildArg, type UINodeChild } from "./UINode"
|
|
4
4
|
|
|
5
5
|
export type UIBottomSheetStyle = UIModalStyle
|
|
6
6
|
|
|
@@ -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 {
|
|
@@ -130,20 +126,14 @@ export class BottomSheetElement extends ModalElement {
|
|
|
130
126
|
}
|
|
131
127
|
}
|
|
132
128
|
|
|
133
|
-
|
|
129
|
+
const makeBottomSheet = (style: any, children: any) => new BottomSheetElement(style, children)
|
|
130
|
+
|
|
131
|
+
/** Create a bottom sheet (see {@link UIBottomSheet}) — content-sized by default; call
|
|
132
|
+
* `.detents([...])` for snap positions. Same argument forms as `UIColumn`. */
|
|
133
|
+
export function UIBottomSheet(...children: UIChildArg[]): UIBottomSheet;
|
|
134
134
|
export function UIBottomSheet(children: UINodeChild[] | ChildrenFn): UIBottomSheet;
|
|
135
|
-
|
|
136
|
-
export function UIBottomSheet(style: UIBottomSheetStyle, children
|
|
137
|
-
export function UIBottomSheet(...args: []
|
|
138
|
-
|
|
139
|
-
return new BottomSheetElement({}, [])
|
|
140
|
-
} else if (args.length === 1) {
|
|
141
|
-
if (Array.isArray(args[0]) || typeof args[0] === "function") {
|
|
142
|
-
return new BottomSheetElement({}, args[0])
|
|
143
|
-
} else {
|
|
144
|
-
return new BottomSheetElement(args[0], [])
|
|
145
|
-
}
|
|
146
|
-
} else {
|
|
147
|
-
return new BottomSheetElement(args[0], args[1])
|
|
148
|
-
}
|
|
135
|
+
/** @deprecated Style as the first argument is legacy — use chained `.style({...})`. */
|
|
136
|
+
export function UIBottomSheet(style: UIBottomSheetStyle, children?: UINodeChild[] | ChildrenFn): UIBottomSheet;
|
|
137
|
+
export function UIBottomSheet(...args: any[]): UIBottomSheet {
|
|
138
|
+
return buildUI(args, null, makeBottomSheet)
|
|
149
139
|
}
|
|
@@ -1,38 +1,24 @@
|
|
|
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
|
+
* style block, the `$pressed` class, or `rippleColor`. */
|
|
9
|
+
export interface UIButton extends UIContainerBase<UIButtonStyle & { onPressed?: UIButtonStyle & { duration?: number } }, DrawableStyle & BaseStyle> {
|
|
8
10
|
readonly type: "button",
|
|
9
11
|
|
|
10
|
-
|
|
11
|
-
animateTo: AnimateStyle<this, DrawableStyle & BaseStyle>,
|
|
12
|
-
animateFrom: AnimateStyle<this, DrawableStyle & BaseStyle>
|
|
13
|
-
|
|
12
|
+
/** Fired on a completed tap/click. */
|
|
14
13
|
onClick(callback: (ev: ClickEventType<UIButton>) => void): this,
|
|
14
|
+
/** Fired when a touch begins; `ev.track(...)` takes over the rest of the gesture (dragging). */
|
|
15
15
|
onTouchStart(callback: (ev: TouchStartEventType<UIButton>) => void): this,
|
|
16
16
|
/** Fired when a finger is held on the button past the long-press threshold. The event can
|
|
17
17
|
* `ev.track(...)` the rest of the gesture (like `onTouchStart`), so a hold can flow straight into
|
|
18
18
|
* dragging the element. A handled long-press suppresses the click that would follow the release. */
|
|
19
19
|
onLongPress(callback: (ev: LongPressEventType<UIButton>) => void): this,
|
|
20
|
+
/** Is a finger currently down on the button? */
|
|
20
21
|
isPressed(): boolean,
|
|
21
|
-
|
|
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
|
-
onLayout(onLayout: OnLayoutCallback): this
|
|
28
|
-
getBoundingClientRect(): BoundingClientRect | null
|
|
29
|
-
|
|
30
|
-
/** Style-class proxy — read `el.class.checked`, set `el.class.checked = true`, toggle with
|
|
31
|
-
* `!el.class.checked`, bind `el.class.done = () => sig.value`, batch/chain `el.class({ … })`.
|
|
32
|
-
* Classes cascade to descendants. See {@link Classes}. */
|
|
33
|
-
readonly class: Classes<this>
|
|
34
|
-
|
|
35
|
-
readonly children: UINodeChild[],
|
|
36
22
|
}
|
|
37
23
|
|
|
38
24
|
|
|
@@ -99,20 +85,17 @@ class ButtonElement extends ContainerElement<"button"> {
|
|
|
99
85
|
// default was a recurring authoring/codegen trap.
|
|
100
86
|
const BUTTON_DEFAULTS = { flexDirection: "row", justifyContent: "center", alignItems: "center" } as const
|
|
101
87
|
|
|
102
|
-
|
|
88
|
+
const makeButton = (style: any, children: any) => new ButtonElement("button", style, children)
|
|
89
|
+
|
|
90
|
+
/** @internal Compiler fast path (chisel `flatten_ui` raw lowering) — see UIContainer. */
|
|
91
|
+
export const __UIButton = (children: UINodeChild[] | ChildrenFn): UIButton => makeButton({ ...BUTTON_DEFAULTS }, children)
|
|
92
|
+
|
|
93
|
+
/** Create a button (row + centered by default — the icon+label shape). Same argument forms as
|
|
94
|
+
* `UIColumn`; attach behavior with `.onClick(...)`. */
|
|
95
|
+
export function UIButton(...children: UIChildArg[]): UIButton;
|
|
103
96
|
export function UIButton(children: UINodeChild[] | ChildrenFn): UIButton;
|
|
104
|
-
|
|
105
|
-
export function UIButton(style: UIButtonStyle, children
|
|
106
|
-
export function UIButton(...args: []
|
|
107
|
-
|
|
108
|
-
return new ButtonElement("button", { ...BUTTON_DEFAULTS }, [])
|
|
109
|
-
} else if (args.length === 1) {
|
|
110
|
-
if (Array.isArray(args[0]) || typeof args[0] === "function") {
|
|
111
|
-
return new ButtonElement("button", { ...BUTTON_DEFAULTS }, args[0])
|
|
112
|
-
} else {
|
|
113
|
-
return new ButtonElement("button", { ...BUTTON_DEFAULTS, ...args[0] }, [])
|
|
114
|
-
}
|
|
115
|
-
} else {
|
|
116
|
-
return new ButtonElement("button", { ...BUTTON_DEFAULTS, ...args[0] }, args[1])
|
|
117
|
-
}
|
|
97
|
+
/** @deprecated Style as the first argument is legacy — use chained `.style({...})`. */
|
|
98
|
+
export function UIButton(style: UIButtonStyle, children?: UINodeChild[] | ChildrenFn): UIButton;
|
|
99
|
+
export function UIButton(...args: any[]): UIButton {
|
|
100
|
+
return buildUI(args, BUTTON_DEFAULTS, makeButton)
|
|
118
101
|
}
|
|
@@ -1,122 +1,60 @@
|
|
|
1
|
-
import { ContainerElement,
|
|
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 extends UIContainerBase<UIContainerStyle> {
|
|
6
7
|
readonly type: "column",
|
|
7
|
-
style: Style<this, UIContainerStyle>,
|
|
8
|
-
animateTo: AnimateStyle<this, UIContainerStyle>
|
|
9
|
-
animateFrom: AnimateStyle<this, UIContainerStyle>
|
|
10
|
-
|
|
11
|
-
append(...nodes: UINodeChild[]): UIColumn
|
|
12
|
-
insert(index: number, ...nodes: UINodeChild[]): UIColumn
|
|
13
|
-
remove(...nodes: UINodeChild[]): UIColumn
|
|
14
|
-
setContent(nodes: UINodeChild[] | ChildrenFn): this
|
|
15
|
-
|
|
16
|
-
onLayout(onLayout: OnLayoutCallback): this
|
|
17
|
-
getBoundingClientRect(): BoundingClientRect | null
|
|
18
|
-
|
|
19
|
-
/** Style-class proxy — read `el.class.checked`, set `el.class.checked = true`, toggle with
|
|
20
|
-
* `!el.class.checked`, bind `el.class.done = () => sig.value`, batch/chain `el.class({ … })`.
|
|
21
|
-
* Classes cascade to descendants. See {@link Classes}. */
|
|
22
|
-
readonly class: Classes<this>
|
|
23
|
-
|
|
24
|
-
readonly children: UINodeChild[]
|
|
25
8
|
}
|
|
26
|
-
|
|
9
|
+
const makeColumn = (style: any, children: any) => new ContainerElement("column", style, children)
|
|
10
|
+
|
|
11
|
+
/** @internal Compiler fast path (chisel `flatten_ui` raw lowering): construction with a proven
|
|
12
|
+
* plain-children array — or a reactive ChildrenFn (the Element constructor branches on
|
|
13
|
+
* `typeof === "function"` either way) — skipping the buildUI argument dispatch entirely. */
|
|
14
|
+
export const __UIColumn = (children: UINodeChild[] | ChildrenFn): UIColumn => makeColumn({}, children)
|
|
15
|
+
export const __UIRow = (children: UINodeChild[] | ChildrenFn): UIRow => makeRow({ flexDirection: "row" }, children)
|
|
16
|
+
export const __UIBox = (children: UINodeChild[] | ChildrenFn): UIBox => makeBox({ ...BOX_DEFAULTS }, children)
|
|
17
|
+
|
|
18
|
+
/** Create a vertical flex container. Children are variadic — arrays flatten one level, falsy
|
|
19
|
+
* entries are skipped; a lone function makes the children reactive. */
|
|
20
|
+
export function UIColumn(...children: UIChildArg[]): UIColumn
|
|
27
21
|
export function UIColumn(children: UINodeChild[] | ChildrenFn): UIColumn
|
|
28
|
-
|
|
29
|
-
export function UIColumn(style: UIContainerStyle, children
|
|
30
|
-
export function UIColumn(...args: []
|
|
31
|
-
|
|
32
|
-
return new ContainerElement("column", {}, [])
|
|
33
|
-
} else if (args.length === 1) {
|
|
34
|
-
if (Array.isArray(args[0]) || typeof args[0] === "function") {
|
|
35
|
-
return new ContainerElement("column", {}, args[0])
|
|
36
|
-
} else {
|
|
37
|
-
return new ContainerElement("column", args[0], [])
|
|
38
|
-
}
|
|
39
|
-
} else {
|
|
40
|
-
return new ContainerElement("column", args[0], args[1])
|
|
41
|
-
}
|
|
22
|
+
/** @deprecated Style as the first argument is legacy — use chained `.style({...})`. */
|
|
23
|
+
export function UIColumn(style: UIContainerStyle, children?: UINodeChild[] | ChildrenFn): UIColumn
|
|
24
|
+
export function UIColumn(...args: any[]): UIColumn {
|
|
25
|
+
return buildUI(args, null, makeColumn)
|
|
42
26
|
}
|
|
43
27
|
|
|
44
|
-
|
|
28
|
+
/** A horizontal flex container — children flow left to right. */
|
|
29
|
+
export interface UIRow extends UIContainerBase<UIContainerStyle> {
|
|
45
30
|
readonly type: "row",
|
|
46
|
-
style: Style<this, UIContainerStyle>,
|
|
47
|
-
animateTo: AnimateStyle<this, UIContainerStyle>,
|
|
48
|
-
animateFrom: AnimateStyle<this, UIContainerStyle>,
|
|
49
|
-
|
|
50
|
-
append(...nodes: UINodeChild[]): UIRow
|
|
51
|
-
insert(index: number, ...nodes: UINodeChild[]): UIRow
|
|
52
|
-
remove(...nodes: UINodeChild[]): UIRow
|
|
53
|
-
setContent(nodes: UINodeChild[] | ChildrenFn): this
|
|
54
|
-
|
|
55
|
-
onLayout(onLayout: OnLayoutCallback): this
|
|
56
|
-
getBoundingClientRect(): BoundingClientRect | null
|
|
57
|
-
|
|
58
|
-
/** Style-class proxy — read `el.class.checked`, set `el.class.checked = true`, toggle with
|
|
59
|
-
* `!el.class.checked`, bind `el.class.done = () => sig.value`, batch/chain `el.class({ … })`.
|
|
60
|
-
* Classes cascade to descendants. See {@link Classes}. */
|
|
61
|
-
readonly class: Classes<this>
|
|
62
|
-
|
|
63
|
-
readonly children: UINodeChild[],
|
|
64
31
|
}
|
|
65
|
-
|
|
32
|
+
const ROW_DEFAULTS = { flexDirection: "row" } as const
|
|
33
|
+
const makeRow = (style: any, children: any) => new ContainerElement("row", style, children)
|
|
34
|
+
|
|
35
|
+
/** Create a horizontal flex container (same argument forms as `UIColumn`). */
|
|
36
|
+
export function UIRow(...children: UIChildArg[]): UIRow
|
|
66
37
|
export function UIRow(children: UINodeChild[] | ChildrenFn): UIRow
|
|
67
|
-
|
|
68
|
-
export function UIRow(style: UIContainerStyle, children
|
|
69
|
-
export function UIRow(...args: []
|
|
70
|
-
|
|
71
|
-
return new ContainerElement("row", { flexDirection: "row" }, [])
|
|
72
|
-
} else if (args.length === 1) {
|
|
73
|
-
if (Array.isArray(args[0]) || typeof args[0] === "function") {
|
|
74
|
-
return new ContainerElement("row", { flexDirection: "row" }, args[0])
|
|
75
|
-
} else {
|
|
76
|
-
return new ContainerElement("row", { flexDirection: "row", ...args[0] }, [])
|
|
77
|
-
}
|
|
78
|
-
} else {
|
|
79
|
-
return new ContainerElement("row", { flexDirection: "row", ...args[0] }, args[1])
|
|
80
|
-
}
|
|
38
|
+
/** @deprecated Style as the first argument is legacy — use chained `.style({...})`. */
|
|
39
|
+
export function UIRow(style: UIContainerStyle, children?: UINodeChild[] | ChildrenFn): UIRow
|
|
40
|
+
export function UIRow(...args: any[]): UIRow {
|
|
41
|
+
return buildUI(args, ROW_DEFAULTS, makeRow)
|
|
81
42
|
}
|
|
82
43
|
|
|
83
|
-
|
|
44
|
+
/** A centering container — `justifyContent`/`alignItems` default to center. */
|
|
45
|
+
export interface UIBox extends UIContainerBase<UIContainerStyle> {
|
|
84
46
|
readonly type: "box",
|
|
85
|
-
style: Style<this, UIContainerStyle>,
|
|
86
|
-
animateTo: AnimateStyle<this, UIContainerStyle>,
|
|
87
|
-
animateFrom: AnimateStyle<this, UIContainerStyle>,
|
|
88
|
-
|
|
89
|
-
append(...nodes: UINodeChild[]): this
|
|
90
|
-
insert(index: number, ...nodes: UINodeChild[]): this
|
|
91
|
-
remove(...nodes: UINodeChild[]): this
|
|
92
|
-
setContent(nodes: UINodeChild[] | ChildrenFn): this
|
|
93
|
-
|
|
94
|
-
onLayout(onLayout: OnLayoutCallback): this
|
|
95
|
-
getBoundingClientRect(): BoundingClientRect | null
|
|
96
|
-
|
|
97
|
-
/** Style-class proxy — read `el.class.checked`, set `el.class.checked = true`, toggle with
|
|
98
|
-
* `!el.class.checked`, bind `el.class.done = () => sig.value`, batch/chain `el.class({ … })`.
|
|
99
|
-
* Classes cascade to descendants. See {@link Classes}. */
|
|
100
|
-
readonly class: Classes<this>
|
|
101
|
-
|
|
102
|
-
readonly children: UINodeChild[],
|
|
103
47
|
}
|
|
104
48
|
const BOX_DEFAULTS = { justifyContent: "center", alignItems: "center" } as const
|
|
105
49
|
|
|
106
|
-
|
|
50
|
+
const makeBox = (style: any, children: any) => new ContainerElement("box", style, children)
|
|
51
|
+
|
|
52
|
+
/** Create a centering container — children are centered on both axes by default (icon badges,
|
|
53
|
+
* avatars, empty states). Same argument forms as `UIColumn`. */
|
|
54
|
+
export function UIBox(...children: UIChildArg[]): UIBox
|
|
107
55
|
export function UIBox(children: UINodeChild[] | ChildrenFn): UIBox
|
|
108
|
-
|
|
109
|
-
export function UIBox(style: UIContainerStyle, children
|
|
110
|
-
export function UIBox(...args: []
|
|
111
|
-
|
|
112
|
-
return new ContainerElement("box", { ...BOX_DEFAULTS }, [])
|
|
113
|
-
} else if (args.length === 1) {
|
|
114
|
-
if (Array.isArray(args[0]) || typeof args[0] === "function") {
|
|
115
|
-
return new ContainerElement("box", { ...BOX_DEFAULTS }, args[0])
|
|
116
|
-
} else {
|
|
117
|
-
return new ContainerElement("box", { ...BOX_DEFAULTS, ...args[0] }, [])
|
|
118
|
-
}
|
|
119
|
-
} else {
|
|
120
|
-
return new ContainerElement("box", { ...BOX_DEFAULTS, ...args[0] }, args[1])
|
|
121
|
-
}
|
|
56
|
+
/** @deprecated Style as the first argument is legacy — use chained `.style({...})`. */
|
|
57
|
+
export function UIBox(style: UIContainerStyle, children?: UINodeChild[] | ChildrenFn): UIBox
|
|
58
|
+
export function UIBox(...args: any[]): UIBox {
|
|
59
|
+
return buildUI(args, BOX_DEFAULTS, makeBox)
|
|
122
60
|
}
|
|
@@ -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
|
|
|
@@ -21,28 +21,18 @@ const toSrc = (src: ImageSource): string | FetchResponse | File | SvgSource | Ca
|
|
|
21
21
|
/** The stored/resolved image source (a Canvas is resolved to a surface marker). */
|
|
22
22
|
export type ResolvedImageSource = string | FetchResponse | File | SvgSource | CanvasImageSource
|
|
23
23
|
|
|
24
|
-
|
|
24
|
+
/** An image element. Sources: URL / `asset(...)`, fetch response / File, an SVG source
|
|
25
|
+
* (`assetIcon`), or a live Canvas (`canvas.update()` refreshes the node in place). */
|
|
26
|
+
export interface UIImage extends UIElementBase<UIImageStyle> {
|
|
25
27
|
readonly type: "image",
|
|
28
|
+
/** The image source — writable to swap the picture in place. */
|
|
26
29
|
get src(): ResolvedImageSource
|
|
27
30
|
set src(value: ImageSource)
|
|
28
|
-
style: Style<this, UIImageStyle>
|
|
29
|
-
animateTo: AnimateStyle<this, UIImageStyle>
|
|
30
|
-
animateFrom: AnimateStyle<this, UIImageStyle>
|
|
31
31
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
// size (one frame, not the whole atlas) and it overrides objectFit — the cropped frame
|
|
36
|
-
// always fills the box.
|
|
32
|
+
/** Render only a sub-rectangle of the source texture (texture pixels) — the atlas/spritesheet
|
|
33
|
+
* primitive: crop once, then swap the rect per frame for sprite animation. The rect's w/h
|
|
34
|
+
* become the node's intrinsic size and override `objectFit` — the cropped frame fills the box. */
|
|
37
35
|
setSourceRect(x: number, y: number, w: number, h: number): this
|
|
38
|
-
|
|
39
|
-
onLayout(onLayout: OnLayoutCallback): this
|
|
40
|
-
getBoundingClientRect(): BoundingClientRect | null
|
|
41
|
-
|
|
42
|
-
/** Style-class proxy — read `el.class.checked`, set `el.class.checked = true`, toggle with
|
|
43
|
-
* `!el.class.checked`, bind `el.class.done = () => sig.value`, batch/chain `el.class({ … })`.
|
|
44
|
-
* Classes cascade to descendants. See {@link Classes}. */
|
|
45
|
-
readonly class: Classes<this>
|
|
46
36
|
}
|
|
47
37
|
|
|
48
38
|
class ImageElement extends Element<"image"> {
|
|
@@ -80,7 +70,9 @@ class ImageElement extends Element<"image"> {
|
|
|
80
70
|
}
|
|
81
71
|
}
|
|
82
72
|
|
|
73
|
+
/** Create an image element from a source (see {@link ImageSource}). */
|
|
83
74
|
export function UIImage(src: ImageSource): UIImage
|
|
75
|
+
/** @deprecated Style as the first argument is legacy — use chained `.style({...})`. */
|
|
84
76
|
export function UIImage(style: UIImageStyle, src: ImageSource): UIImage
|
|
85
77
|
export function UIImage(...args: [ImageSource] | [UIImageStyle, ImageSource]): UIImage {
|
|
86
78
|
if (args.length === 1) {
|
|
@@ -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,
|
|
@@ -49,7 +49,9 @@ type SubmitStyle = {
|
|
|
49
49
|
enterKey?: "done" | "go" | "next" | "search" | "send",
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
|
|
52
|
+
/** A single-line text input. Keyboard/picker kind via the `type` style, return-key behavior via
|
|
53
|
+
* `enterKey` + `onSubmit`; keyboard policy on `keyboardShrink`/`keyboardDismiss`. */
|
|
54
|
+
export interface UIInput extends UIElementBase<UIInputStyle & SubmitStyle & { onFocused?: UIInputStyle & { duration?: number } }, DrawableStyle & BaseStyle> {
|
|
53
55
|
readonly type: "input",
|
|
54
56
|
|
|
55
57
|
/**
|
|
@@ -60,55 +62,39 @@ export interface UIInput {
|
|
|
60
62
|
*/
|
|
61
63
|
value: string
|
|
62
64
|
|
|
63
|
-
|
|
64
|
-
animateTo: AnimateStyle<this, DrawableStyle & BaseStyle>
|
|
65
|
-
animateFrom: AnimateStyle<this, DrawableStyle & BaseStyle>
|
|
66
|
-
|
|
65
|
+
/** The text changed — fires on every user edit. */
|
|
67
66
|
onChange(callback: (value: string) => void): UIInput
|
|
67
|
+
/** Gained focus (keyboard opens). */
|
|
68
68
|
onFocus(callback: () => void): UIInput
|
|
69
|
+
/** Lost focus. */
|
|
69
70
|
onBlur(callback: () => void): UIInput
|
|
70
71
|
/** The keyboard's return key was pressed. `value` is the current text. See `enterKey`. */
|
|
71
72
|
onSubmit(callback: (value: string) => void): UIInput
|
|
72
|
-
onLayout(onLayout: OnLayoutCallback): this
|
|
73
|
-
getBoundingClientRect(): BoundingClientRect | null
|
|
74
73
|
|
|
75
74
|
/** Focus the input (opens the keyboard). No-op before mount. */
|
|
76
75
|
focus(): void
|
|
77
76
|
/** Release focus (dismisses the keyboard when this input held it). */
|
|
78
77
|
blur(): void
|
|
79
|
-
|
|
80
|
-
/** Style-class proxy — read `el.class.checked`, set `el.class.checked = true`, toggle with
|
|
81
|
-
* `!el.class.checked`, bind `el.class.done = () => sig.value`, batch/chain `el.class({ … })`.
|
|
82
|
-
* Classes cascade to descendants. See {@link Classes}. */
|
|
83
|
-
readonly class: Classes<this>
|
|
84
78
|
}
|
|
85
79
|
|
|
86
|
-
|
|
80
|
+
/** A multi-line text field — see the {@link UITextArea} factory for the auto-grow contract. */
|
|
81
|
+
export interface UITextArea extends UIElementBase<UIInputStyle & { onFocused?: UIInputStyle & { duration?: number } }, DrawableStyle & BaseStyle> {
|
|
87
82
|
readonly type: "textarea",
|
|
88
83
|
|
|
89
84
|
/** The field's text — same contract as {@link UIInput.value}: settable before mount to pre-fill. */
|
|
90
85
|
value: string
|
|
91
86
|
|
|
92
|
-
|
|
93
|
-
// styleV: StyleFn<this, UIInputStyle>,
|
|
94
|
-
// styleH: StyleFn<this, UIInputStyle>,
|
|
95
|
-
|
|
96
|
-
// appear: AppearStyle<this, UIInputStyle>
|
|
97
|
-
// disappear: DisappearStyle<this, UIInputStyle>
|
|
98
|
-
|
|
87
|
+
/** The text changed — fires on every user edit. */
|
|
99
88
|
onChange(callback: (value: string) => void): UITextArea
|
|
89
|
+
/** Gained focus (keyboard opens). */
|
|
100
90
|
onFocus(callback: () => void): UITextArea
|
|
91
|
+
/** Lost focus. */
|
|
101
92
|
onBlur(callback: () => void): UITextArea
|
|
102
93
|
|
|
103
94
|
/** Focus the text area (opens the keyboard). No-op before mount. */
|
|
104
95
|
focus(): void
|
|
105
96
|
/** Release focus (dismisses the keyboard when this text area held it). */
|
|
106
97
|
blur(): void
|
|
107
|
-
|
|
108
|
-
/** Style-class proxy — read `el.class.checked`, set `el.class.checked = true`, toggle with
|
|
109
|
-
* `!el.class.checked`, bind `el.class.done = () => sig.value`, batch/chain `el.class({ … })`.
|
|
110
|
-
* Classes cascade to descendants. See {@link Classes}. */
|
|
111
|
-
readonly class: Classes<this>
|
|
112
98
|
}
|
|
113
99
|
|
|
114
100
|
class InputElement<T extends "input" | "textarea"> extends Element<T> {
|
|
@@ -175,6 +161,8 @@ class InputElement<T extends "input" | "textarea"> extends Element<T> {
|
|
|
175
161
|
}
|
|
176
162
|
}
|
|
177
163
|
|
|
164
|
+
/** Create a single-line text input. Read/write the text via `.value`, listen with
|
|
165
|
+
* `onChange`/`onSubmit`; style keys `placeholder`, `type`, `enterKey`, … configure it. */
|
|
178
166
|
export function UIInput(): UIInput;
|
|
179
167
|
export function UIInput(style: UIInputStyle): UIInput;
|
|
180
168
|
export function UIInput(style?: UIInputStyle): UIInput {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type UIWidget, type UIWidgetStyle, WidgetElement } from "./UIWidget"
|
|
2
|
-
import type
|
|
2
|
+
import { buildUI, type BaseStyle, type ChildrenFn, type Color, type DrawableStyle, type UIChildArg, type UINodeChild } from "./UINode"
|
|
3
3
|
|
|
4
4
|
export type UIModalStyle = UIWidgetStyle
|
|
5
5
|
|
|
@@ -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 {
|
|
@@ -130,20 +126,14 @@ export class ModalElement extends WidgetElement {
|
|
|
130
126
|
}
|
|
131
127
|
}
|
|
132
128
|
|
|
133
|
-
|
|
129
|
+
const makeModal = (style: any, children: any) => new ModalElement(style, children)
|
|
130
|
+
|
|
131
|
+
/** Create a modal dialog (see {@link UIModal}) — create once at module scope, then
|
|
132
|
+
* `show()`/`hide()`. Same argument forms as `UIColumn`. */
|
|
133
|
+
export function UIModal(...children: UIChildArg[]): UIModal;
|
|
134
134
|
export function UIModal(children: UINodeChild[] | ChildrenFn): UIModal;
|
|
135
|
-
|
|
136
|
-
export function UIModal(style: UIModalStyle, children
|
|
137
|
-
export function UIModal(...args: []
|
|
138
|
-
|
|
139
|
-
return new ModalElement({}, [])
|
|
140
|
-
} else if (args.length === 1) {
|
|
141
|
-
if (Array.isArray(args[0]) || typeof args[0] === "function") {
|
|
142
|
-
return new ModalElement({}, args[0])
|
|
143
|
-
} else {
|
|
144
|
-
return new ModalElement(args[0], [])
|
|
145
|
-
}
|
|
146
|
-
} else {
|
|
147
|
-
return new ModalElement(args[0], args[1])
|
|
148
|
-
}
|
|
135
|
+
/** @deprecated Style as the first argument is legacy — use chained `.style({...})`. */
|
|
136
|
+
export function UIModal(style: UIModalStyle, children?: UINodeChild[] | ChildrenFn): UIModal;
|
|
137
|
+
export function UIModal(...args: any[]): UIModal {
|
|
138
|
+
return buildUI(args, null, makeModal)
|
|
149
139
|
}
|