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.
@@ -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 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
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, type Classes, type Style, ContainerElement, buildUI, type DrawableStyle, type UIChildArg, 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
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 AnimateStyle, type AppearStyle, type ChildrenFn, type ContainerStyle, type DisappearStyle, type DrawableStyle, type ElementStyle, type OnLayoutCallback, type BoundingClientRect, type Classes, type Style, type StyleFn, type UIChildArg, 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
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 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
 
@@ -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, type Classes, type Style, 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,
@@ -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
- style: Style<this, UIInputStyle & SubmitStyle & { onFocused?: UIInputStyle & { duration?: number } }>,
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
- style: Style<this, UIInputStyle & { onFocused?: UIInputStyle & { duration?: number } }>,
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 {
@@ -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 AnimateStyle, type BaseStyle, type ChildrenFn, type DrawableStyle, type ElementStyle, type OnLayoutCallback, type BoundingClientRect, type Classes, type Style, type UINodeChild } from "./UINode"
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 AnimateStyle, type BaseStyle, type ContainerStyle, type DrawableStyle, type OnLayoutCallback, type BoundingClientRect, type PaddingStyle, type Classes, type Style, type UIChildArg, type UINodeChild, type ChildrenFn, } from "./UINode"
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
- onLayout(onLayout: OnLayoutCallback): this
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 Classes, type Style, type AnimateStyle, type UIChildArg, type UINodeChild, type ChildrenFn, type OnLayoutCallback, type BoundingClientRect, type Color, ContainerElement, buildUI } from "./UINode"
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 Classes, type Style, type StyleFn } from "./UINode"
1
+ import { Element, type ElementStyle, type UIElementBase } from "./UINode"
2
2
 
3
3
  export type UISpacerStyle = ElementStyle
4
4
 
5
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
@@ -1,24 +1,13 @@
1
1
  import { createBinding } from "../core/signals"
2
- import { Element, type AnimateStyle, type BaseStyle, type DrawableStyle, type ElementStyle, type OnLayoutCallback, type BoundingClientRect, type Classes, type Style, type TextStyle } from "./UINode"
2
+ import { Element, type BaseStyle, type DrawableStyle, type ElementStyle, type TextStyle, type UIElementBase } from "./UINode"
3
3
 
4
4
  export type UITextStyle = ElementStyle & TextStyle & DrawableStyle
5
5
 
6
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"> {