lecodes-cli 0.10.4 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/README.md +1 -1
  2. package/dist/index.js +48887 -6261
  3. package/package.json +4 -3
  4. package/runtime/scene-harness.json +1 -1
  5. package/runtime/sdk/compile/aspectMacro.ts +42 -0
  6. package/runtime/sdk/compile/assetIconMacro.ts +382 -0
  7. package/runtime/sdk/compile/assetMacro.ts +45 -0
  8. package/runtime/sdk/compile/assetName.ts +50 -0
  9. package/runtime/sdk/compile/bundler.ts +244 -0
  10. package/runtime/sdk/compile/compileProject.ts +106 -0
  11. package/runtime/sdk/compile/detectEntry.ts +125 -0
  12. package/runtime/sdk/compile/fontMacro.ts +459 -0
  13. package/runtime/sdk/compile/fontRegistry.ts +78 -0
  14. package/runtime/sdk/compile/header.ts +55 -0
  15. package/runtime/sdk/compile/index.ts +53 -0
  16. package/runtime/sdk/compile/libraryImports.ts +52 -0
  17. package/runtime/sdk/compile/sceneEditor.ts +78 -0
  18. package/runtime/sdk/compile/sfnt.ts +98 -0
  19. package/runtime/sdk/compile/sourcemap.ts +25 -0
  20. package/runtime/sdk/g2/Node2D.ts +3 -0
  21. package/runtime/sdk/g2/SpriteAnimation.ts +101 -20
  22. package/runtime/sdk/g2/SpriteSheet.ts +166 -0
  23. package/runtime/sdk/g2/Tileset.ts +71 -0
  24. package/runtime/sdk/g2/autotile.ts +394 -0
  25. package/runtime/sdk/g2/cells.ts +91 -0
  26. package/runtime/sdk/g2/defineScene2d.ts +381 -0
  27. package/runtime/sdk/g2/scenarios2d.ts +69 -0
  28. package/runtime/sdk/inject.ts +18 -0
  29. package/runtime/sdk/runtime/app.ts +21 -3
  30. package/runtime/sdk/scene/defineScene.ts +10 -70
  31. package/runtime/sdk/scene/grammar.ts +85 -0
  32. package/runtime/sdk/ui/NativeView.ts +9 -12
  33. package/runtime/sdk/ui/UI.ts +9 -1
  34. package/runtime/sdk/ui/UIBottomSheet.ts +10 -20
  35. package/runtime/sdk/ui/UIButton.ts +19 -36
  36. package/runtime/sdk/ui/UIContainer.ts +41 -103
  37. package/runtime/sdk/ui/UIImage.ts +10 -18
  38. package/runtime/sdk/ui/UIInput.ts +14 -26
  39. package/runtime/sdk/ui/UIModal.ts +10 -20
  40. package/runtime/sdk/ui/UINode.ts +142 -36
  41. package/runtime/sdk/ui/UIPager.ts +29 -28
  42. package/runtime/sdk/ui/UIPopover.ts +10 -16
  43. package/runtime/sdk/ui/UIScreen.ts +23 -40
  44. package/runtime/sdk/ui/UIScrollable.ts +37 -47
  45. package/runtime/sdk/ui/UISpacer.ts +4 -8
  46. package/runtime/sdk/ui/UITabs.ts +175 -0
  47. package/runtime/sdk/ui/UIText.ts +7 -13
  48. package/runtime/sdk/ui/UIVideo.ts +11 -13
  49. package/runtime/sdk/ui/UIVirtualizedList.ts +43 -49
  50. package/runtime/sdk/ui/UIWidget.ts +24 -36
  51. package/runtime/sdk/ui/fonts.ts +3 -0
  52. package/runtime/sdk/ui/router.ts +13 -0
  53. package/runtime/sdk/ui/theme.ts +21 -30
  54. package/runtime/sdk-types.json +1 -1
@@ -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 AnimateStyle, type BaseStyle, type DrawableStyle, type ElementStyle, type OnLayoutCallback, type BoundingClientRect, type Classes, type Style } from "./UINode"
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
- export interface NativeView {
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
@@ -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 { ChildrenFn, UINodeChild } from "./UINode"
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
- export function UIBottomSheet(): UIBottomSheet;
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
- export function UIBottomSheet(style: UIBottomSheetStyle): UIBottomSheet;
136
- export function UIBottomSheet(style: UIBottomSheetStyle, children: UINodeChild[] | ChildrenFn): UIBottomSheet;
137
- export function UIBottomSheet(...args: [] | [UINodeChild[] | ChildrenFn | UIBottomSheetStyle] | [UIBottomSheetStyle, children: UINodeChild[] | ChildrenFn]): UIBottomSheet {
138
- if (args.length === 0) {
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, type Classes, type Style, ContainerElement, type DrawableStyle, type UINodeChild, type ChildrenFn, type OnLayoutCallback, type BoundingClientRect, type BaseStyle, type AnimateStyle, type Color } from "./UINode"
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
- export interface UIButton {
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
- style: Style<this, UIButtonStyle & { onPressed?: UIButtonStyle & { duration?: number } }>,
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
- export function UIButton(): UIButton;
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
- export function UIButton(style: UIButtonStyle): UIButton;
105
- export function UIButton(style: UIButtonStyle, children: UINodeChild[]): UIButton;
106
- export function UIButton(...args: [] | [UINodeChild[] | ChildrenFn | UIButtonStyle] | [UIButtonStyle, children: UINodeChild[] | ChildrenFn]): UIButton {
107
- if (args.length === 0) {
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, type AnimateStyle, type AppearStyle, type ChildrenFn, type ContainerStyle, type DisappearStyle, type DrawableStyle, type ElementStyle, type OnLayoutCallback, type BoundingClientRect, type Classes, type Style, type StyleFn, type UINode, type UINodeChild } from "./UINode"
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
- export interface UIColumn {
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
- export function UIColumn(): UIColumn
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
- export function UIColumn(style: UIContainerStyle): UIColumn
29
- export function UIColumn(style: UIContainerStyle, children: UINodeChild[]): UIColumn
30
- export function UIColumn(...args: [] | [UINodeChild[] | ChildrenFn | UIContainerStyle] | [UIContainerStyle, children: UINodeChild[] | ChildrenFn]): UIColumn {
31
- if (args.length === 0) {
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
- export interface UIRow {
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
- export function UIRow(): UIRow
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
- export function UIRow(style: UIContainerStyle): UIRow
68
- export function UIRow(style: UIContainerStyle, children: UINodeChild[]): UIRow
69
- export function UIRow(...args: [] | [UINodeChild[] | ChildrenFn | UIContainerStyle] | [UIContainerStyle, children: UINodeChild[] | ChildrenFn]): UIRow {
70
- if (args.length === 0) {
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
- export interface UIBox {
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
- export function UIBox(): UIBox
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
- export function UIBox(style: UIContainerStyle): UIBox
109
- export function UIBox(style: UIContainerStyle, children: UINodeChild[]): UIBox
110
- export function UIBox(...args: [] | [UINodeChild[] | ChildrenFn | UIContainerStyle] | [UIContainerStyle, children: UINodeChild[] | ChildrenFn]): UIBox {
111
- if (args.length === 0) {
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 AnimateStyle, type Color, type ElementStyle, type OnLayoutCallback, type BoundingClientRect, type Classes, type Style } from "./UINode"
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
- export interface UIImage {
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
- // Render only a sub-rectangle of the source texture (coords/size in texture pixels).
33
- // This is the atlas / spritesheet primitive: the initial crop is the first call, sprite
34
- // animation is swapping the rect per frame. The rect's w/h become the node's intrinsic
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, type Classes, type Style, type StyleFn, type AppearStyle, type DisappearStyle, Element, type TextStyle, type Color, type OnLayoutCallback, type BoundingClientRect, type BaseStyle, type AnimateStyle } from "./UINode";
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
- export interface UIInput {
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
- style: Style<this, UIInputStyle & SubmitStyle & { onFocused?: UIInputStyle & { duration?: number } }>,
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
- export interface UITextArea {
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
- style: Style<this, UIInputStyle & { onFocused?: UIInputStyle & { duration?: number } }>,
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 { BaseStyle, ChildrenFn, Color, DrawableStyle, UINodeChild } from "./UINode"
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
- export function UIModal(): UIModal;
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
- export function UIModal(style: UIModalStyle): UIModal;
136
- export function UIModal(style: UIModalStyle, children: UINodeChild[] | ChildrenFn): UIModal;
137
- export function UIModal(...args: [] | [UINodeChild[] | ChildrenFn | UIModalStyle] | [UIModalStyle, children: UINodeChild[] | ChildrenFn]): UIModal {
138
- if (args.length === 0) {
139
- return new ModalElement({}, [])
140
- } else if (args.length === 1) {
141
- if (Array.isArray(args[0]) || typeof args[0] === "function") {
142
- return new ModalElement({}, args[0])
143
- } else {
144
- return new ModalElement(args[0], [])
145
- }
146
- } else {
147
- return new ModalElement(args[0], args[1])
148
- }
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
  }