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
@@ -10,10 +10,19 @@ export interface UINode {
10
10
  readonly class: Classes<this>
11
11
  }
12
12
 
13
+ /** A child slot value: a node, or falsy (conditional rendering — falsy entries are skipped). */
13
14
  export type UINodeChild = UINode | null | undefined | false
14
15
 
16
+ /** A factory child argument: a node (or falsy for conditionals), or an array of them — arrays are
17
+ * flattened one level at the argument position, so `UIColumn(header, items.map(row), footer)`
18
+ * needs no spread. */
19
+ export type UIChildArg = UINodeChild | UINodeChild[]
20
+
21
+ /** A color: CSS-style string (`"#1c1c1e"`, `"rgba(0,0,0,0.5)"`, `"var(--primaryColor)"`) or
22
+ * packed number; `null` clears. */
15
23
  export type Color = number | string | null
16
24
 
25
+ /** Style props every element accepts. */
17
26
  export type BaseStyle = {
18
27
  opacity?: number | `${number}`,
19
28
  transform?: string | null,
@@ -29,6 +38,7 @@ export type BaseStyle = {
29
38
  name?: string,
30
39
  }
31
40
 
41
+ /** Painted-box props: background, gradient, border, radius. */
32
42
  export type DrawableStyle = {
33
43
  backgroundImage?: FetchResponse | File | string | null,
34
44
  bgGradient?: string,
@@ -62,6 +72,7 @@ export type DrawableStyle = {
62
72
  pointerEvents?: "all" | "none",
63
73
  }
64
74
 
75
+ /** Flex-layout props of a container (how it lays out its children). */
65
76
  export type ContainerStyle = {
66
77
  flexDirection?: "row" | "column",
67
78
  justifyContent?: "flex-start" | "center" | "flex-end" | "space-between" | "space-evenly",
@@ -70,6 +81,7 @@ export type ContainerStyle = {
70
81
  flexWrap?: "nowrap" | "wrap" | "wrap-reverse"
71
82
  }
72
83
 
84
+ /** A dimension: a number (logical px) or a CSS-style unit / function string. */
73
85
  type UIValue = number |
74
86
  `${number}px` |
75
87
  `${number}vw` |
@@ -84,6 +96,8 @@ type UIValue = number |
84
96
  `clamp(${string})` |
85
97
  `var(--${string})`
86
98
 
99
+ /** Padding, with `p`/`px`/`pl`… shorthands. `safe-*` insets by the device safe area,
100
+ * `comfort-*` by the theme's comfort knobs (max of both). */
87
101
  export type PaddingStyle = {
88
102
  paddingLeft?: UIValue | "safe-left" | "comfort-left",
89
103
  paddingTop?: UIValue | "safe-top" | "comfort-top",
@@ -102,8 +116,7 @@ export type PaddingStyle = {
102
116
  py?: UIValue | "comfort-y",
103
117
  }
104
118
 
105
-
106
-
119
+ /** Margin, with `m`/`mx`/`ml`… shorthands; `"auto"` centers/pushes like CSS. */
107
120
  export type MarginStyle = {
108
121
  marginLeft?: UIValue | "auto" | "safe-left" | "comfort-left",
109
122
  marginTop?: UIValue | "auto" | "safe-top" | "comfort-top",
@@ -122,6 +135,7 @@ export type MarginStyle = {
122
135
  my?: UIValue | "auto" | "comfort-y",
123
136
  }
124
137
 
138
+ /** Box position (for `position: "absolute"`) and size constraints. */
125
139
  export type PositionStyle = {
126
140
  top?: UIValue | `${number}%` | 'safe-top' | 'comfort-top',
127
141
  left?: UIValue | `${number}%` | 'safe-left' | 'comfort-left',
@@ -137,6 +151,7 @@ export type PositionStyle = {
137
151
  minHeight?: UIValue | `${number}%` | "auto",
138
152
  }
139
153
 
154
+ /** The base style of every element: box + flex-child props. */
140
155
  export type ElementStyle = PaddingStyle & MarginStyle & BaseStyle & PositionStyle & {
141
156
  position?: "absolute" | "static" | "relative",
142
157
  aspectRatio?: number,
@@ -148,6 +163,7 @@ export type ElementStyle = PaddingStyle & MarginStyle & BaseStyle & PositionStyl
148
163
  display?: "none" | "flex"
149
164
  }
150
165
 
166
+ /** Typography props. */
151
167
  export type TextStyle = {
152
168
  textAlign?: "start" | "center" | "end" | "left" | "right"
153
169
  fontFamily?: string,
@@ -189,7 +205,7 @@ function applyBindings(el: any, fns: [string, () => any][]) {
189
205
  }
190
206
  }
191
207
 
192
- // Не надо переписывать эту функцию 10 раз, обрати внимание, что у тебя тут возвращается this
208
+ // NB: returns `this` — `.style()` must chain. Don't rewrite.
193
209
  function styleFunction(this: any, newStyle: any) {
194
210
  // `name` is a JS-node selector (read by hosts off el.name), not a layout property — extract it
195
211
  // exactly like the constructor so `.style({ name })` is equivalent to the first-arg style. Left
@@ -305,26 +321,21 @@ const classHandler: ProxyHandler<any> = {
305
321
  }
306
322
  }
307
323
 
308
- function Style() {
309
-
310
- }
311
-
312
- // Reactive style input: every primitive-valued prop also accepts a `() => value` binding that
313
- // re-applies when a signal it read changes. Nested state blocks (onPressed, onLandscape, …) stay
314
- // static — bindings are extracted at the top level only.
324
+ /** Reactive style input: every primitive-valued prop also accepts a `() => value` binding that
325
+ * re-applies when a signal it read changes. Nested state blocks (onPressed, onLandscape, …) stay
326
+ * static — bindings are extracted at the top level only. */
315
327
  export type Reactive<T> = { [K in keyof T]: NonNullable<T[K]> extends object ? T[K] : T[K] | (() => T[K]) }
316
328
 
317
- // User-defined style classes: any `$`-prefixed key in .style() declares a state block (like
318
- // onPressed, but with any name), toggled from code via the `el.class` proxy. `duration`/`delay`
319
- // make the swap transition. Class state INHERITS down the tree (CSS-`.dark`-on-body style): a
320
- // class set on a node also activates same-name `$` blocks on all descendants within the same
321
- // root — see docs/style-class-cascade-plan.md. `$pressed`/`$focused` are reserved: hosts toggle
322
- // them on press/focus (they beat other classes, lose to onPressed/onFocused), so children can
323
- // react to an ancestor's press. Instantiate with the node's own style `T` (see StyleFn below) — a
324
- // class block accepts everything the node's `.style()` does (e.g. `color` on text, layout on any
325
- // node), exactly like `onLandscape`/`onPortrait`. Narrowing it (it once was `DrawableStyle &
326
- // BaseStyle`) wrongly rejected valid props such as `$active: { color }`. Guarded by
327
- // tests/ui-types.test.ts.
329
+ /** User-defined style classes: any `$`-prefixed key in `.style()` declares a state block toggled
330
+ * from code via the `el.class` proxy; `duration`/`delay` animate the swap. Class state INHERITS
331
+ * down the tree (CSS-`.dark`-on-body style): a class set on a node also activates same-name `$`
332
+ * blocks on all descendants within the same root (docs/style-class-cascade-plan.md).
333
+ * `$pressed`/`$focused` are reserved — hosts toggle them on press/focus (they beat other
334
+ * classes, lose to onPressed/onFocused), so children can react to an ancestor's press.
335
+ *
336
+ * Instantiated with the node's FULL style `T` — a class block accepts everything the node's
337
+ * `.style()` does, like `onLandscape`. Narrowing it wrongly rejects valid props such as
338
+ * `$active: { color }`; guarded by tests/ui-types.test.ts. */
328
339
  export type ClassStyles<T> = { [key: `$${string}`]: T & { duration?: number, delay?: number } }
329
340
 
330
341
  /** Value accepted by a `el.class` write: a boolean sets the class, a `() => boolean` binds it —
@@ -351,15 +362,46 @@ export type ClassValue = boolean | (() => boolean)
351
362
  * single-key writes are runtime-coerced with `!!`. */
352
363
  export type Classes<R> = ((classes: Record<string, ClassValue>) => R) & { [key: string]: any }
353
364
 
354
- // NB: do NOT annotate `this: R` here. `this` is a parameter position, so it makes R contravariant,
355
- // which makes every node type INVARIANT in its own type params (and in `this`). That is what made
356
- // `UIScreen<false>` unassignable to `UIScreen<boolean>` (a navigator page), and would block any
357
- // UINode subtype from being assignable to its base. Runtime `this` is already fixed by the
358
- // `styleFunction.bind(this)` in the `style` getter — the annotation bought nothing and cost variance.
359
- // R stays only in the (covariant) return, so `.style()` still returns the concrete node type for
360
- // chaining. Guarded by the type tests in tests/ui-types.test-d.ts.
361
- // Orientation state blocks — override props applied only in landscape / portrait (like a `$`-class,
362
- // keyed by device orientation instead of `el.class`). Same full-`T` rule as class blocks.
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
+
402
+ /** Orientation state blocks — override props applied only in landscape / portrait (like a
403
+ * `$`-class, keyed by device orientation instead of `el.class`). Same full-`T` rule as class
404
+ * blocks. */
363
405
  export type OrientationStyles<T> = {
364
406
  onLandscape?: T,
365
407
  onPortrait?: T,
@@ -368,11 +410,19 @@ export type OrientationStyles<T> = {
368
410
  onPortait?: T,
369
411
  }
370
412
 
413
+ // NB: do NOT annotate `this: R` on StyleFn. As a parameter it makes R contravariant, turning every
414
+ // node type INVARIANT (UIScreen<false> stopped being assignable to UIScreen<boolean>). Runtime
415
+ // `this` is already fixed by `styleFunction.bind(this)`; R stays covariant in the return, so
416
+ // `.style()` still chains. Guarded by tests/ui-types.test-d.ts.
371
417
  export type StyleFn <R, T extends object> = ((style: Reactive<T> & OrientationStyles<T> & ClassStyles<T>) => R)
418
+
419
+ /** The `el.style` surface: callable — `.style({...})` merges and returns the element for
420
+ * chaining — and per-key readable/writable (`el.style.opacity = 0.5`; a `() => value` write
421
+ * installs a reactive binding). */
372
422
  export type Style <R, T extends object> = StyleFn<R,T> & T & OrientationStyles<T> & ClassStyles<T>
373
423
 
374
- // The options bag of animateTo/animateFrom: the target style plus flat meta keys. Meta keys are
375
- // stripped before the commit into _style and skipped by the hosts' style staging.
424
+ /** The `animateTo`/`animateFrom` surface: target style props plus the flat meta keys below
425
+ * (stripped before the commit into the element's style). */
376
426
  export type AnimateStyle<R, T extends object> = ((style: T & {
377
427
  /** Tween length in **milliseconds** (default 225). */
378
428
  duration?: number,
@@ -397,10 +447,11 @@ const ANIMATE_META = new Set(["duration", "delay", "layer", "commit", "loop", "l
397
447
  export type AppearStyle <R, T> = (arg: { from: T, duration?: number, delay?: number }) => R
398
448
  export type DisappearStyle <R, T> = (arg: { to: T, duration?: number, delay?: number }) => R
399
449
 
450
+ /** `onLayout` payload — the node's box in PARENT-relative coordinates. */
400
451
  export type OnLayoutCallback = (layout: { left: number, top: number, width: number, height: number }) => void
401
452
 
402
- // Web-DOMRect shape on purpose (getBoundingClientRect priors must hold): all eight fields, with
403
- // right/bottom/x/y derived SDK-side so hosts only report [left, top, width, height].
453
+ /** Web-DOMRect shape on purpose (getBoundingClientRect priors must hold): all eight fields, with
454
+ * right/bottom/x/y derived SDK-side — hosts only report [left, top, width, height]. */
404
455
  export type BoundingClientRect = {
405
456
  x: number, y: number,
406
457
  left: number, top: number, right: number, bottom: number,
@@ -462,10 +513,10 @@ export class Element<T extends string> {
462
513
  return proxy
463
514
  }
464
515
 
516
+ /** Tween to the target style and commit it (unless `commit: false` or looping — a loop is an
517
+ * effect, not a state change). Meta keys: `duration`/`delay`/`loop`/… — see {@link AnimateStyle}. */
465
518
  animateTo(style: any): this{
466
519
  _creatorUI.animateTo(this._id, style)
467
- // A loop is an effect, not a state change — looping animations never commit. Meta keys
468
- // (duration/delay/…) stay out of the stored style.
469
520
  if (style.commit !== false && !style.loop) {
470
521
  for (const key in style) {
471
522
  if (!ANIMATE_META.has(key)) this._style[key] = style[key]
@@ -473,6 +524,7 @@ export class Element<T extends string> {
473
524
  }
474
525
  return this
475
526
  }
527
+ /** Tween FROM the given style to the element's current one (entrance animations). */
476
528
  animateFrom(style: any): this{
477
529
  _creatorUI.animateFrom(this._id, style)
478
530
  return this
@@ -511,6 +563,7 @@ export class Element<T extends string> {
511
563
  }
512
564
 
513
565
  protected ll?: OnLayoutCallback[]
566
+ /** Observe layout: fires after every host layout pass with the parent-relative box. */
514
567
  onLayout(onLayout: OnLayoutCallback): this {
515
568
  if (this.ll) {
516
569
  this.ll.push(onLayout)
@@ -536,6 +589,8 @@ export class Element<T extends string> {
536
589
 
537
590
  // ---- reactive children --------------------------------------------------------------------------
538
591
 
592
+ /** Reactive children: re-runs when a signal it read changes; the result is reconciled against
593
+ * the mounted children (kept nodes stay mounted, state intact). */
539
594
  export type ChildrenFn = () => UINodeChild[]
540
595
 
541
596
  // The container whose children binding is currently executing. __uiMap uses it as the cache owner;
@@ -580,6 +635,53 @@ export function __uiMap(list: any, render: (item: any, index: number) => any, sl
580
635
  return out
581
636
  }
582
637
 
638
+ // ---- factory argument dispatch ------------------------------------------------------------------
639
+
640
+ /** @internal Shared argument dispatch for the UI factories (`UIRow(...)`, `UIButton(...)`, …).
641
+ * Children are variadic; an array argument is flattened one level, so both the legacy
642
+ * `UIColumn([a, b])` and `UIColumn(header, items.map(row), footer)` work. A lone function
643
+ * argument is a reactive {@link ChildrenFn}. A plain non-node object in first position is the
644
+ * legacy style bag: it is spread over `defaults`, or — when `defaults` is null — stored as-is
645
+ * (the Element constructor mutates it in place, matching the old path). The rest-args array is
646
+ * taken over as the children array when nothing needs flattening — callers must hand it off. */
647
+ export function buildUI<R>(args: any[], defaults: any, make: (style: any, children: any[] | ChildrenFn) => R): R {
648
+ const n = args.length
649
+ const a0 = n > 0 ? args[0] : undefined
650
+ let style: any
651
+ let start = 0
652
+ if (a0 !== null && typeof a0 === "object" && !Array.isArray(a0) && !(a0 instanceof Element)) {
653
+ style = defaults ? { ...defaults, ...a0 } : a0
654
+ start = 1
655
+ } else {
656
+ style = defaults ? { ...defaults } : {}
657
+ }
658
+ if (n === start + 1) {
659
+ const a = args[start]
660
+ // lone function → reactive children; lone array → used as the children array directly
661
+ // (the legacy zero-copy path — nested arrays inside it are NOT flattened)
662
+ if (typeof a === "function" || Array.isArray(a)) return make(style, a)
663
+ }
664
+ // Single pass: reuse the rest-args array as the children array until the first array argument
665
+ // shows up, then copy the prefix once and flatten from there. Falsy entries stay — they are
666
+ // compacted at mount like always.
667
+ let flat: any[] | undefined
668
+ for (let i = start; i < n; i++) {
669
+ const a = args[i]
670
+ if (flat === undefined) {
671
+ if (Array.isArray(a)) {
672
+ flat = args.slice(start, i)
673
+ for (let j = 0; j < a.length; j++) flat.push(a[j])
674
+ }
675
+ } else if (Array.isArray(a)) {
676
+ for (let j = 0; j < a.length; j++) flat.push(a[j])
677
+ } else {
678
+ flat.push(a)
679
+ }
680
+ }
681
+ if (flat !== undefined) return make(style, flat)
682
+ return make(style, start === 0 ? args : args.slice(1))
683
+ }
684
+
583
685
  export class ContainerElement<T extends string> extends Element<T> {
584
686
  children: any[] = []
585
687
 
@@ -641,6 +743,7 @@ export class ContainerElement<T extends string> extends Element<T> {
641
743
  }
642
744
  }
643
745
 
746
+ /** Append children (falsy entries are kept in the array but never mounted). */
644
747
  append(...nodes: UINodeChild[]): this {
645
748
  if (this._id === 0) {
646
749
  this.children.push(...nodes)
@@ -660,6 +763,7 @@ export class ContainerElement<T extends string> extends Element<T> {
660
763
  this.children.push(...nodes)
661
764
  return this
662
765
  }
766
+ /** Insert children at `index` of the children array. */
663
767
  insert(index: number, ...nodes: UINodeChild[]): this {
664
768
  if (this._id === 0) {
665
769
  this.children.splice(index, 0, ...nodes)
@@ -680,6 +784,7 @@ export class ContainerElement<T extends string> extends Element<T> {
680
784
  this.children.splice(index, 0, ...nodes)
681
785
  return this
682
786
  }
787
+ /** Remove (unmount) the given children. */
683
788
  remove(...nodesToDelete: UINode[]): this {
684
789
  const set = new Set(nodesToDelete)
685
790
 
@@ -704,6 +809,7 @@ export class ContainerElement<T extends string> extends Element<T> {
704
809
  return this
705
810
  }
706
811
 
812
+ /** Replace all children — a plain array, or a function for reactive children. */
707
813
  setContent(children: UINodeChild[] | ChildrenFn): this {
708
814
  if (typeof children === "function") {
709
815
  this._bindChildren(children) // createBinding replaces a previous "#children" binding
@@ -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"> {
@@ -104,7 +93,10 @@ export class PagerElement extends ContainerElement<"pager"> {
104
93
  private ncl: ((depth: number) => void)[] = [] // onChange listeners
105
94
 
106
95
  constructor(style: UIPagerStyle | null, tabs: UIScreen[]) {
107
- super("pager", style ?? {}, tabs as any)
96
+ // flexShrink: 1 default (SDK-owned, like UIScrollable's): pages are separate layout roots
97
+ // sized to the pager's box, so yoga sees no shrinkable content — without this a pager in a
98
+ // flex column overflows the screen instead of leaving room for the tab bar.
99
+ super("pager", { flexShrink: 1, ...style }, tabs as any)
108
100
  this._lens = tabs.map(() => 1)
109
101
  for (const s of this.children) this._track(s)
110
102
  }
@@ -294,13 +286,13 @@ const isScreenArg = (v: any): boolean => v != null && typeof v === "object" && v
294
286
  /** Callable factory + ambient statics. The name `UIPager` is a value (this factory) and a type
295
287
  * (the instance interface above) — a standard value/type merge. */
296
288
  interface UIPagerConstructor {
297
- /** One tab — a plain navigation stack rooted at `root` (push/pop, no tab swiping). */
298
- (root: UIScreen): UIPager
299
- /** Sibling tabs, one per screen, in order — swipe (or `select`) between them. Tabs are fixed
300
- * at construction; each starts as its own one-screen stack. */
301
- (tabs: UIScreen[]): UIPager
302
- /** Styled pager + content (a single root screen or an array of tab screens). */
303
- (style: UIPagerStyle, content: UIScreen | UIScreen[]): UIPager
289
+ /** Tabs — each argument is a screen or an array of screens (arrays flatten one level, falsy
290
+ * entries are skipped). One screen → a plain navigation stack rooted at it (push/pop, no tab
291
+ * swiping); several → swipeable sibling tabs, fixed at construction, each starting as its own
292
+ * one-screen stack. */
293
+ (...tabs: (UIScreen | UIScreen[] | false | null | undefined)[]): UIPager
294
+ /** @deprecated Style as the first argument is legacy — use chained `.style({...})`. */
295
+ (style: UIPagerStyle, content?: UIScreen | UIScreen[]): UIPager
304
296
  /** The pager that owns the currently visible screen, or `null` when the visible screen isn't
305
297
  * hosted by one. With pagers nested inside pages, resolves to the **innermost** — so ambient
306
298
  * calls act on the pager the user is actually looking at. */
@@ -316,14 +308,23 @@ interface UIPagerConstructor {
316
308
  }
317
309
 
318
310
  function pagerFactory(...args: any[]): UIPager {
319
- if (args.length === 1) {
320
- const a = args[0]
321
- if (Array.isArray(a)) return new PagerElement(null, a) as unknown as UIPager
322
- if (isScreenArg(a)) return new PagerElement(null, [a]) as unknown as UIPager
323
- return new PagerElement(a, []) as unknown as UIPager
311
+ let style: any = null
312
+ let start = 0
313
+ const a0 = args.length > 0 ? args[0] : undefined
314
+ if (a0 != null && typeof a0 === "object" && !Array.isArray(a0) && !isScreenArg(a0)) {
315
+ style = a0
316
+ start = 1
317
+ }
318
+ const tabs: any[] = []
319
+ for (let i = start; i < args.length; i++) {
320
+ const a = args[i]
321
+ if (Array.isArray(a)) {
322
+ for (let j = 0; j < a.length; j++) if (a[j]) tabs.push(a[j])
323
+ } else if (a) {
324
+ tabs.push(a)
325
+ }
324
326
  }
325
- const content = args[1]
326
- return new PagerElement(args[0] ?? null, Array.isArray(content) ? content : content ? [content] : []) as unknown as UIPager
327
+ return new PagerElement(style, tabs) as unknown as UIPager
327
328
  }
328
329
 
329
330
  /**
@@ -1,7 +1,7 @@
1
1
  import { ModalElement, type UIModal, type UIModalStyle } from "./UIModal"
2
2
  import { Presentable } from "./presentable"
3
3
  import { device } from "../runtime/device"
4
- import type { BoundingClientRect, ChildrenFn, UINodeChild } from "./UINode"
4
+ import { buildUI, type BoundingClientRect, type ChildrenFn, type UIChildArg, type UINodeChild } from "./UINode"
5
5
 
6
6
  export type UIPopoverStyle = UIModalStyle
7
7
 
@@ -87,20 +87,14 @@ export class PopoverElement extends ModalElement {
87
87
  }
88
88
  }
89
89
 
90
- export function UIPopover(): UIPopover;
90
+ const makePopover = (style: any, children: any) => new PopoverElement(style, children)
91
+
92
+ /** Create an anchored popover (see {@link UIPopover}) — create once at module scope, then
93
+ * `show(anchor)` next to an element or `{x, y}` point. Same argument forms as `UIColumn`. */
94
+ export function UIPopover(...children: UIChildArg[]): UIPopover;
91
95
  export function UIPopover(children: UINodeChild[] | ChildrenFn): UIPopover;
92
- export function UIPopover(style: UIPopoverStyle): UIPopover;
93
- export function UIPopover(style: UIPopoverStyle, children: UINodeChild[] | ChildrenFn): UIPopover;
94
- export function UIPopover(...args: [] | [UINodeChild[] | ChildrenFn | UIPopoverStyle] | [UIPopoverStyle, children: UINodeChild[] | ChildrenFn]): UIPopover {
95
- if (args.length === 0) {
96
- return new PopoverElement({}, [])
97
- } else if (args.length === 1) {
98
- if (Array.isArray(args[0]) || typeof args[0] === "function") {
99
- return new PopoverElement({}, args[0])
100
- } else {
101
- return new PopoverElement(args[0], [])
102
- }
103
- } else {
104
- return new PopoverElement(args[0], args[1])
105
- }
96
+ /** @deprecated Style as the first argument is legacy — use chained `.style({...})`. */
97
+ export function UIPopover(style: UIPopoverStyle, children?: UINodeChild[] | ChildrenFn): UIPopover;
98
+ export function UIPopover(...args: any[]): UIPopover {
99
+ return buildUI(args, null, makePopover)
106
100
  }
@@ -1,35 +1,28 @@
1
1
  import { TouchStartEvent } from "../runtime/touch"
2
2
  import { _bumpNavEpoch, _navSupported, _setCurrent, Presentable, type PresentOptions } from "./presentable"
3
- import { ContainerElement, type AnimateStyle, type BaseStyle, type ContainerStyle, type DrawableStyle, type OnLayoutCallback, type BoundingClientRect, type PaddingStyle, type Classes, type Style, 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
 
7
- // A screen — the root the router/host mounts, sized to its slot. A screen NEVER scrolls: it is
8
- // fixed chrome (header, tab bar) plus, when the content overflows, one `UIScrollable` body child
9
- // (`flexGrow: 1`). Scroll events and pull-to-refresh live on the scroll containers
10
- // (`UIScrollable`, `UIVirtualizedList`) — see docs/scroll-forms-plan.md (the former
11
- // `makeScrollable()` screen mode was removed 2026-07-22).
12
- export interface UIScreen {
7
+ /** A screen — the root the router/host mounts, sized to its slot. A screen NEVER scrolls: it is
8
+ * fixed chrome (header, tab bar) plus, when the content overflows, one `UIScrollable` body child
9
+ * (`flexGrow: 1`). Scroll events and pull-to-refresh live on the scroll containers
10
+ * (`UIScrollable`, `UIVirtualizedList`). */
11
+ export interface UIScreen extends UIContainerBase<UIScreenStyle, DrawableStyle & BaseStyle> {
13
12
  readonly type: "screen",
14
- style: Style<this, UIScreenStyle>,
15
- animateTo: AnimateStyle<this, DrawableStyle & BaseStyle>
16
- animateFrom: AnimateStyle<this, DrawableStyle & BaseStyle>
17
13
 
18
14
  /** Show this screen as the current destination (replaces whatever is visible — a screen, a
19
15
  * scene, a native view — suspending an active Router until `Router.restore()`). */
20
16
  open(options?: PresentOptions): void,
17
+ /** Dismiss if this screen is the visible destination. */
21
18
  close(): void
22
19
 
23
- append(...nodes: UINodeChild[]): this
24
- insert(index: number, ...nodes: UINodeChild[]): this
25
- remove(...nodes: UINodeChild[]): this
26
- setContent(nodes: UINodeChild[] | ChildrenFn): this
27
-
28
- readonly children: UINodeChild[],
29
-
20
+ /** Hardware/system back while this screen is current. */
30
21
  onBackPressed(callback: () => void): this
31
22
 
23
+ /** The screen became the visible destination (first open, or revealed by a pop). */
32
24
  onOpen(callback: () => void): this
25
+ /** The screen stopped being visible — closed, replaced, or covered by a push. */
33
26
  onClose(callback: () => void): this
34
27
 
35
28
  /**
@@ -43,14 +36,7 @@ export interface UIScreen {
43
36
  /** Free a `keepAlive()` screen's retained tree now. No-op unless it's currently detached-and-kept. */
44
37
  dispose(): this
45
38
 
46
- onLayout(onLayout: OnLayoutCallback): this
47
- getBoundingClientRect(): BoundingClientRect | null
48
-
49
- /** Style-class proxy — read `el.class.checked`, set `el.class.checked = true`, toggle with
50
- * `!el.class.checked`, bind `el.class.done = () => sig.value`, batch/chain `el.class({ … })`.
51
- * Classes cascade to descendants. See {@link Classes}. */
52
- readonly class: Classes<this>
53
-
39
+ /** Touch began on the screen; `ev.track(...)` takes over the rest of the gesture. */
54
40
  onTouchStart(callback: (ev: TouchStartEvent<UIScreen>) => void): this,
55
41
  }
56
42
 
@@ -121,20 +107,17 @@ export class ScreenElement extends ContainerElement<"screen"> {
121
107
  }
122
108
  }
123
109
 
124
- export function UIScreen(): UIScreen;
110
+ const makeScreen = (style: any, children: any) => new ScreenElement("screen", style, children)
111
+
112
+ /** @internal Compiler fast path (chisel `flatten_ui` raw lowering) — see UIContainer. */
113
+ export const __UIScreen = (children: UINodeChild[] | ChildrenFn): UIScreen => makeScreen({}, children)
114
+
115
+ /** Create a screen. Present it with `.open()`, or via `Router` / `UIPager`. Same argument forms
116
+ * as `UIColumn`. */
117
+ export function UIScreen(...children: UIChildArg[]): UIScreen;
125
118
  export function UIScreen(children: UINodeChild[] | ChildrenFn): UIScreen;
126
- export function UIScreen(style: UIScreenStyle): UIScreen;
127
- export function UIScreen(style: UIScreenStyle, children: UINodeChild[]): UIScreen;
128
- export function UIScreen(...args: [] | [UINodeChild[] | ChildrenFn | UIScreenStyle] | [UIScreenStyle, children: UINodeChild[] | ChildrenFn]): UIScreen {
129
- if (args.length === 0) {
130
- return new ScreenElement("screen", {}, [])
131
- } else if (args.length === 1) {
132
- if (Array.isArray(args[0]) || typeof args[0] === "function") {
133
- return new ScreenElement("screen", {}, args[0])
134
- } else {
135
- return new ScreenElement("screen", args[0], [])
136
- }
137
- } else {
138
- return new ScreenElement("screen", args[0], args[1])
139
- }
119
+ /** @deprecated Style as the first argument is legacy — use chained `.style({...})`. */
120
+ export function UIScreen(style: UIScreenStyle, children?: UINodeChild[] | ChildrenFn): UIScreen;
121
+ export function UIScreen(...args: any[]): UIScreen {
122
+ return buildUI(args, null, makeScreen)
140
123
  }