lecodes-cli 0.10.4 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/index.js +3117 -1575
- package/package.json +1 -1
- package/runtime/scene-harness.json +1 -1
- package/runtime/sdk/compile/aspectMacro.ts +42 -0
- package/runtime/sdk/compile/assetIconMacro.ts +382 -0
- package/runtime/sdk/compile/assetMacro.ts +45 -0
- package/runtime/sdk/compile/assetName.ts +50 -0
- package/runtime/sdk/compile/bundler.ts +244 -0
- package/runtime/sdk/compile/compileProject.ts +106 -0
- package/runtime/sdk/compile/detectEntry.ts +125 -0
- package/runtime/sdk/compile/fontMacro.ts +459 -0
- package/runtime/sdk/compile/fontRegistry.ts +78 -0
- package/runtime/sdk/compile/header.ts +55 -0
- package/runtime/sdk/compile/index.ts +53 -0
- package/runtime/sdk/compile/libraryImports.ts +52 -0
- package/runtime/sdk/compile/sceneEditor.ts +78 -0
- package/runtime/sdk/compile/sfnt.ts +98 -0
- package/runtime/sdk/compile/sourcemap.ts +25 -0
- package/runtime/sdk/inject.ts +2 -0
- package/runtime/sdk/ui/NativeView.ts +3 -0
- package/runtime/sdk/ui/UI.ts +9 -1
- package/runtime/sdk/ui/UIBottomSheet.ts +10 -16
- package/runtime/sdk/ui/UIButton.ts +18 -16
- package/runtime/sdk/ui/UIContainer.ts +38 -46
- package/runtime/sdk/ui/UIImage.ts +8 -5
- package/runtime/sdk/ui/UIInput.ts +6 -6
- package/runtime/sdk/ui/UIModal.ts +10 -16
- package/runtime/sdk/ui/UINode.ts +105 -36
- package/runtime/sdk/ui/UIPager.ts +27 -15
- package/runtime/sdk/ui/UIPopover.ts +10 -16
- package/runtime/sdk/ui/UIScreen.ts +20 -21
- package/runtime/sdk/ui/UIScrollable.ts +37 -29
- package/runtime/sdk/ui/UISpacer.ts +2 -0
- package/runtime/sdk/ui/UITabs.ts +175 -0
- package/runtime/sdk/ui/UIText.ts +5 -0
- package/runtime/sdk/ui/UIVideo.ts +5 -0
- package/runtime/sdk/ui/UIVirtualizedList.ts +33 -39
- package/runtime/sdk/ui/UIWidget.ts +22 -18
- package/runtime/sdk/ui/fonts.ts +3 -0
- package/runtime/sdk/ui/router.ts +13 -0
- package/runtime/sdk/ui/theme.ts +21 -30
- package/runtime/sdk-types.json +1 -1
package/runtime/sdk/ui/UINode.ts
CHANGED
|
@@ -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
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
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,9 @@ 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
|
-
|
|
355
|
-
|
|
356
|
-
|
|
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
|
+
/** Orientation state blocks — override props applied only in landscape / portrait (like a
|
|
366
|
+
* `$`-class, keyed by device orientation instead of `el.class`). Same full-`T` rule as class
|
|
367
|
+
* blocks. */
|
|
363
368
|
export type OrientationStyles<T> = {
|
|
364
369
|
onLandscape?: T,
|
|
365
370
|
onPortrait?: T,
|
|
@@ -368,11 +373,19 @@ export type OrientationStyles<T> = {
|
|
|
368
373
|
onPortait?: T,
|
|
369
374
|
}
|
|
370
375
|
|
|
376
|
+
// NB: do NOT annotate `this: R` on StyleFn. As a parameter it makes R contravariant, turning every
|
|
377
|
+
// node type INVARIANT (UIScreen<false> stopped being assignable to UIScreen<boolean>). Runtime
|
|
378
|
+
// `this` is already fixed by `styleFunction.bind(this)`; R stays covariant in the return, so
|
|
379
|
+
// `.style()` still chains. Guarded by tests/ui-types.test-d.ts.
|
|
371
380
|
export type StyleFn <R, T extends object> = ((style: Reactive<T> & OrientationStyles<T> & ClassStyles<T>) => R)
|
|
381
|
+
|
|
382
|
+
/** The `el.style` surface: callable — `.style({...})` merges and returns the element for
|
|
383
|
+
* chaining — and per-key readable/writable (`el.style.opacity = 0.5`; a `() => value` write
|
|
384
|
+
* installs a reactive binding). */
|
|
372
385
|
export type Style <R, T extends object> = StyleFn<R,T> & T & OrientationStyles<T> & ClassStyles<T>
|
|
373
386
|
|
|
374
|
-
|
|
375
|
-
|
|
387
|
+
/** The `animateTo`/`animateFrom` surface: target style props plus the flat meta keys below
|
|
388
|
+
* (stripped before the commit into the element's style). */
|
|
376
389
|
export type AnimateStyle<R, T extends object> = ((style: T & {
|
|
377
390
|
/** Tween length in **milliseconds** (default 225). */
|
|
378
391
|
duration?: number,
|
|
@@ -397,10 +410,11 @@ const ANIMATE_META = new Set(["duration", "delay", "layer", "commit", "loop", "l
|
|
|
397
410
|
export type AppearStyle <R, T> = (arg: { from: T, duration?: number, delay?: number }) => R
|
|
398
411
|
export type DisappearStyle <R, T> = (arg: { to: T, duration?: number, delay?: number }) => R
|
|
399
412
|
|
|
413
|
+
/** `onLayout` payload — the node's box in PARENT-relative coordinates. */
|
|
400
414
|
export type OnLayoutCallback = (layout: { left: number, top: number, width: number, height: number }) => void
|
|
401
415
|
|
|
402
|
-
|
|
403
|
-
|
|
416
|
+
/** Web-DOMRect shape on purpose (getBoundingClientRect priors must hold): all eight fields, with
|
|
417
|
+
* right/bottom/x/y derived SDK-side — hosts only report [left, top, width, height]. */
|
|
404
418
|
export type BoundingClientRect = {
|
|
405
419
|
x: number, y: number,
|
|
406
420
|
left: number, top: number, right: number, bottom: number,
|
|
@@ -462,10 +476,10 @@ export class Element<T extends string> {
|
|
|
462
476
|
return proxy
|
|
463
477
|
}
|
|
464
478
|
|
|
479
|
+
/** Tween to the target style and commit it (unless `commit: false` or looping — a loop is an
|
|
480
|
+
* effect, not a state change). Meta keys: `duration`/`delay`/`loop`/… — see {@link AnimateStyle}. */
|
|
465
481
|
animateTo(style: any): this{
|
|
466
482
|
_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
483
|
if (style.commit !== false && !style.loop) {
|
|
470
484
|
for (const key in style) {
|
|
471
485
|
if (!ANIMATE_META.has(key)) this._style[key] = style[key]
|
|
@@ -473,6 +487,7 @@ export class Element<T extends string> {
|
|
|
473
487
|
}
|
|
474
488
|
return this
|
|
475
489
|
}
|
|
490
|
+
/** Tween FROM the given style to the element's current one (entrance animations). */
|
|
476
491
|
animateFrom(style: any): this{
|
|
477
492
|
_creatorUI.animateFrom(this._id, style)
|
|
478
493
|
return this
|
|
@@ -511,6 +526,7 @@ export class Element<T extends string> {
|
|
|
511
526
|
}
|
|
512
527
|
|
|
513
528
|
protected ll?: OnLayoutCallback[]
|
|
529
|
+
/** Observe layout: fires after every host layout pass with the parent-relative box. */
|
|
514
530
|
onLayout(onLayout: OnLayoutCallback): this {
|
|
515
531
|
if (this.ll) {
|
|
516
532
|
this.ll.push(onLayout)
|
|
@@ -536,6 +552,8 @@ export class Element<T extends string> {
|
|
|
536
552
|
|
|
537
553
|
// ---- reactive children --------------------------------------------------------------------------
|
|
538
554
|
|
|
555
|
+
/** Reactive children: re-runs when a signal it read changes; the result is reconciled against
|
|
556
|
+
* the mounted children (kept nodes stay mounted, state intact). */
|
|
539
557
|
export type ChildrenFn = () => UINodeChild[]
|
|
540
558
|
|
|
541
559
|
// The container whose children binding is currently executing. __uiMap uses it as the cache owner;
|
|
@@ -580,6 +598,53 @@ export function __uiMap(list: any, render: (item: any, index: number) => any, sl
|
|
|
580
598
|
return out
|
|
581
599
|
}
|
|
582
600
|
|
|
601
|
+
// ---- factory argument dispatch ------------------------------------------------------------------
|
|
602
|
+
|
|
603
|
+
/** @internal Shared argument dispatch for the UI factories (`UIRow(...)`, `UIButton(...)`, …).
|
|
604
|
+
* Children are variadic; an array argument is flattened one level, so both the legacy
|
|
605
|
+
* `UIColumn([a, b])` and `UIColumn(header, items.map(row), footer)` work. A lone function
|
|
606
|
+
* argument is a reactive {@link ChildrenFn}. A plain non-node object in first position is the
|
|
607
|
+
* legacy style bag: it is spread over `defaults`, or — when `defaults` is null — stored as-is
|
|
608
|
+
* (the Element constructor mutates it in place, matching the old path). The rest-args array is
|
|
609
|
+
* taken over as the children array when nothing needs flattening — callers must hand it off. */
|
|
610
|
+
export function buildUI<R>(args: any[], defaults: any, make: (style: any, children: any[] | ChildrenFn) => R): R {
|
|
611
|
+
const n = args.length
|
|
612
|
+
const a0 = n > 0 ? args[0] : undefined
|
|
613
|
+
let style: any
|
|
614
|
+
let start = 0
|
|
615
|
+
if (a0 !== null && typeof a0 === "object" && !Array.isArray(a0) && !(a0 instanceof Element)) {
|
|
616
|
+
style = defaults ? { ...defaults, ...a0 } : a0
|
|
617
|
+
start = 1
|
|
618
|
+
} else {
|
|
619
|
+
style = defaults ? { ...defaults } : {}
|
|
620
|
+
}
|
|
621
|
+
if (n === start + 1) {
|
|
622
|
+
const a = args[start]
|
|
623
|
+
// lone function → reactive children; lone array → used as the children array directly
|
|
624
|
+
// (the legacy zero-copy path — nested arrays inside it are NOT flattened)
|
|
625
|
+
if (typeof a === "function" || Array.isArray(a)) return make(style, a)
|
|
626
|
+
}
|
|
627
|
+
// Single pass: reuse the rest-args array as the children array until the first array argument
|
|
628
|
+
// shows up, then copy the prefix once and flatten from there. Falsy entries stay — they are
|
|
629
|
+
// compacted at mount like always.
|
|
630
|
+
let flat: any[] | undefined
|
|
631
|
+
for (let i = start; i < n; i++) {
|
|
632
|
+
const a = args[i]
|
|
633
|
+
if (flat === undefined) {
|
|
634
|
+
if (Array.isArray(a)) {
|
|
635
|
+
flat = args.slice(start, i)
|
|
636
|
+
for (let j = 0; j < a.length; j++) flat.push(a[j])
|
|
637
|
+
}
|
|
638
|
+
} else if (Array.isArray(a)) {
|
|
639
|
+
for (let j = 0; j < a.length; j++) flat.push(a[j])
|
|
640
|
+
} else {
|
|
641
|
+
flat.push(a)
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
if (flat !== undefined) return make(style, flat)
|
|
645
|
+
return make(style, start === 0 ? args : args.slice(1))
|
|
646
|
+
}
|
|
647
|
+
|
|
583
648
|
export class ContainerElement<T extends string> extends Element<T> {
|
|
584
649
|
children: any[] = []
|
|
585
650
|
|
|
@@ -641,6 +706,7 @@ export class ContainerElement<T extends string> extends Element<T> {
|
|
|
641
706
|
}
|
|
642
707
|
}
|
|
643
708
|
|
|
709
|
+
/** Append children (falsy entries are kept in the array but never mounted). */
|
|
644
710
|
append(...nodes: UINodeChild[]): this {
|
|
645
711
|
if (this._id === 0) {
|
|
646
712
|
this.children.push(...nodes)
|
|
@@ -660,6 +726,7 @@ export class ContainerElement<T extends string> extends Element<T> {
|
|
|
660
726
|
this.children.push(...nodes)
|
|
661
727
|
return this
|
|
662
728
|
}
|
|
729
|
+
/** Insert children at `index` of the children array. */
|
|
663
730
|
insert(index: number, ...nodes: UINodeChild[]): this {
|
|
664
731
|
if (this._id === 0) {
|
|
665
732
|
this.children.splice(index, 0, ...nodes)
|
|
@@ -680,6 +747,7 @@ export class ContainerElement<T extends string> extends Element<T> {
|
|
|
680
747
|
this.children.splice(index, 0, ...nodes)
|
|
681
748
|
return this
|
|
682
749
|
}
|
|
750
|
+
/** Remove (unmount) the given children. */
|
|
683
751
|
remove(...nodesToDelete: UINode[]): this {
|
|
684
752
|
const set = new Set(nodesToDelete)
|
|
685
753
|
|
|
@@ -704,6 +772,7 @@ export class ContainerElement<T extends string> extends Element<T> {
|
|
|
704
772
|
return this
|
|
705
773
|
}
|
|
706
774
|
|
|
775
|
+
/** Replace all children — a plain array, or a function for reactive children. */
|
|
707
776
|
setContent(children: UINodeChild[] | ChildrenFn): this {
|
|
708
777
|
if (typeof children === "function") {
|
|
709
778
|
this._bindChildren(children) // createBinding replaces a previous "#children" binding
|
|
@@ -104,7 +104,10 @@ export class PagerElement extends ContainerElement<"pager"> {
|
|
|
104
104
|
private ncl: ((depth: number) => void)[] = [] // onChange listeners
|
|
105
105
|
|
|
106
106
|
constructor(style: UIPagerStyle | null, tabs: UIScreen[]) {
|
|
107
|
-
|
|
107
|
+
// flexShrink: 1 default (SDK-owned, like UIScrollable's): pages are separate layout roots
|
|
108
|
+
// sized to the pager's box, so yoga sees no shrinkable content — without this a pager in a
|
|
109
|
+
// flex column overflows the screen instead of leaving room for the tab bar.
|
|
110
|
+
super("pager", { flexShrink: 1, ...style }, tabs as any)
|
|
108
111
|
this._lens = tabs.map(() => 1)
|
|
109
112
|
for (const s of this.children) this._track(s)
|
|
110
113
|
}
|
|
@@ -294,13 +297,13 @@ const isScreenArg = (v: any): boolean => v != null && typeof v === "object" && v
|
|
|
294
297
|
/** Callable factory + ambient statics. The name `UIPager` is a value (this factory) and a type
|
|
295
298
|
* (the instance interface above) — a standard value/type merge. */
|
|
296
299
|
interface UIPagerConstructor {
|
|
297
|
-
/**
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
*
|
|
301
|
-
(tabs: UIScreen[]): UIPager
|
|
302
|
-
/**
|
|
303
|
-
(style: UIPagerStyle, content
|
|
300
|
+
/** Tabs — each argument is a screen or an array of screens (arrays flatten one level, falsy
|
|
301
|
+
* entries are skipped). One screen → a plain navigation stack rooted at it (push/pop, no tab
|
|
302
|
+
* swiping); several → swipeable sibling tabs, fixed at construction, each starting as its own
|
|
303
|
+
* one-screen stack. */
|
|
304
|
+
(...tabs: (UIScreen | UIScreen[] | false | null | undefined)[]): UIPager
|
|
305
|
+
/** @deprecated Style as the first argument is legacy — use chained `.style({...})`. */
|
|
306
|
+
(style: UIPagerStyle, content?: UIScreen | UIScreen[]): UIPager
|
|
304
307
|
/** The pager that owns the currently visible screen, or `null` when the visible screen isn't
|
|
305
308
|
* hosted by one. With pagers nested inside pages, resolves to the **innermost** — so ambient
|
|
306
309
|
* calls act on the pager the user is actually looking at. */
|
|
@@ -316,14 +319,23 @@ interface UIPagerConstructor {
|
|
|
316
319
|
}
|
|
317
320
|
|
|
318
321
|
function pagerFactory(...args: any[]): UIPager {
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
322
|
+
let style: any = null
|
|
323
|
+
let start = 0
|
|
324
|
+
const a0 = args.length > 0 ? args[0] : undefined
|
|
325
|
+
if (a0 != null && typeof a0 === "object" && !Array.isArray(a0) && !isScreenArg(a0)) {
|
|
326
|
+
style = a0
|
|
327
|
+
start = 1
|
|
324
328
|
}
|
|
325
|
-
const
|
|
326
|
-
|
|
329
|
+
const tabs: any[] = []
|
|
330
|
+
for (let i = start; i < args.length; i++) {
|
|
331
|
+
const a = args[i]
|
|
332
|
+
if (Array.isArray(a)) {
|
|
333
|
+
for (let j = 0; j < a.length; j++) if (a[j]) tabs.push(a[j])
|
|
334
|
+
} else if (a) {
|
|
335
|
+
tabs.push(a)
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
return new PagerElement(style, tabs) as unknown as UIPager
|
|
327
339
|
}
|
|
328
340
|
|
|
329
341
|
/**
|
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
93
|
-
export function UIPopover(style: UIPopoverStyle, children
|
|
94
|
-
export function UIPopover(...args: []
|
|
95
|
-
|
|
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,14 +1,13 @@
|
|
|
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 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"
|
|
4
4
|
|
|
5
5
|
export type UIScreenStyle = ContainerStyle & DrawableStyle & PaddingStyle & BaseStyle
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
// `makeScrollable()` screen mode was removed 2026-07-22).
|
|
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`). */
|
|
12
11
|
export interface UIScreen {
|
|
13
12
|
readonly type: "screen",
|
|
14
13
|
style: Style<this, UIScreenStyle>,
|
|
@@ -27,9 +26,12 @@ export interface UIScreen {
|
|
|
27
26
|
|
|
28
27
|
readonly children: UINodeChild[],
|
|
29
28
|
|
|
29
|
+
/** Hardware/system back while this screen is current. */
|
|
30
30
|
onBackPressed(callback: () => void): this
|
|
31
31
|
|
|
32
|
+
/** The screen became the visible destination (first open, or revealed by a pop). */
|
|
32
33
|
onOpen(callback: () => void): this
|
|
34
|
+
/** The screen stopped being visible — closed, replaced, or covered by a push. */
|
|
33
35
|
onClose(callback: () => void): this
|
|
34
36
|
|
|
35
37
|
/**
|
|
@@ -121,20 +123,17 @@ export class ScreenElement extends ContainerElement<"screen"> {
|
|
|
121
123
|
}
|
|
122
124
|
}
|
|
123
125
|
|
|
124
|
-
|
|
126
|
+
const makeScreen = (style: any, children: any) => new ScreenElement("screen", style, children)
|
|
127
|
+
|
|
128
|
+
/** @internal Compiler fast path (chisel `flatten_ui` raw lowering) — see UIContainer. */
|
|
129
|
+
export const __UIScreen = (children: UINodeChild[] | ChildrenFn): UIScreen => makeScreen({}, children)
|
|
130
|
+
|
|
131
|
+
/** Create a screen. Present it with `.open()`, or via `Router` / `UIPager`. Same argument forms
|
|
132
|
+
* as `UIColumn`. */
|
|
133
|
+
export function UIScreen(...children: UIChildArg[]): UIScreen;
|
|
125
134
|
export function UIScreen(children: UINodeChild[] | ChildrenFn): UIScreen;
|
|
126
|
-
|
|
127
|
-
export function UIScreen(style: UIScreenStyle, children
|
|
128
|
-
export function UIScreen(...args: []
|
|
129
|
-
|
|
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
|
-
}
|
|
135
|
+
/** @deprecated Style as the first argument is legacy — use chained `.style({...})`. */
|
|
136
|
+
export function UIScreen(style: UIScreenStyle, children?: UINodeChild[] | ChildrenFn): UIScreen;
|
|
137
|
+
export function UIScreen(...args: any[]): UIScreen {
|
|
138
|
+
return buildUI(args, null, makeScreen)
|
|
140
139
|
}
|
|
@@ -1,19 +1,26 @@
|
|
|
1
1
|
import type { UIContainerStyle } from "./UIContainer"
|
|
2
|
-
import { type Classes, type Style, type AnimateStyle, type UINodeChild, type ChildrenFn, type OnLayoutCallback, type BoundingClientRect, type Color, ContainerElement } from "./UINode"
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
2
|
+
import { type Classes, type Style, type AnimateStyle, type UIChildArg, type UINodeChild, type ChildrenFn, type OnLayoutCallback, type BoundingClientRect, type Color, ContainerElement, buildUI } from "./UINode"
|
|
3
|
+
|
|
4
|
+
export type ScrollStyle = {
|
|
5
|
+
showScrollbar?: boolean,
|
|
6
|
+
overscrollMode?: "none" | "absorb" | "default",
|
|
7
|
+
/** Tint of the native pull-to-refresh spinner (pairs with `onRefresh`). */
|
|
8
|
+
refreshControlColor?: Color,
|
|
9
|
+
/** How a scroll gesture in THIS scrollable dismisses the keyboard: `"interactive"` (default) —
|
|
10
|
+
* dragging over the keyboard slides it away (chat feel); `"scroll"` — any drag dismisses
|
|
11
|
+
* immediately; `"none"` — never (tap-outside policy still applies). */
|
|
12
|
+
keyboardDismissMode?: "interactive" | "scroll" | "none",
|
|
13
|
+
/** Paging: when the drag ends, rest on a direct child's boundary along the scroll axis — the
|
|
14
|
+
* value picks where the child sits in the viewport; `"none"` (default) is free scrolling. Snap
|
|
15
|
+
* targets are the children themselves, so item widths can differ (card-carousel-with-peek); a
|
|
16
|
+
* carousel is a horizontal scrollable of full-width children + `snap: "start"`. */
|
|
17
|
+
snap?: "none" | "start" | "center" | "end"
|
|
18
|
+
}
|
|
14
19
|
|
|
15
20
|
export type UIScrollableStyle = UIContainerStyle & ScrollStyle & { scrollDirection?: "horizontal" | "vertical" }
|
|
16
21
|
|
|
22
|
+
/** A scroll container — vertical by default, `scrollDirection: "horizontal"` for carousels/chip
|
|
23
|
+
* rows. The scrolling body of a screen (screens themselves never scroll). */
|
|
17
24
|
export interface UIScrollable {
|
|
18
25
|
readonly type: "scrollable",
|
|
19
26
|
style: Style<this, UIScrollableStyle>,
|
|
@@ -33,8 +40,11 @@ export interface UIScrollable {
|
|
|
33
40
|
* Classes cascade to descendants. See {@link Classes}. */
|
|
34
41
|
readonly class: Classes<this>
|
|
35
42
|
|
|
43
|
+
/** Scroll position changed (px from the start edge). */
|
|
36
44
|
onScroll(callback: (scrollPosition: number) => void): UIScrollable,
|
|
45
|
+
/** The scroll gesture ended (finger lifted). */
|
|
37
46
|
onScrollRelease(callback: () => void): UIScrollable,
|
|
47
|
+
/** Pulled past the content edge by `delta` px. */
|
|
38
48
|
onOverscroll(callback: (delta: number) => void): UIScrollable
|
|
39
49
|
/** Pull-to-refresh: the spinner stays until the returned promise settles. Attach BEFORE the
|
|
40
50
|
* node mounts (hosts read it at creation). Vertical only; native hosts — no-op on web. */
|
|
@@ -76,22 +86,20 @@ class ScrollableElement extends ContainerElement<"scrollable"> {
|
|
|
76
86
|
}
|
|
77
87
|
}
|
|
78
88
|
|
|
79
|
-
|
|
89
|
+
// flexShrink: 1 default (SDK-owned, like UIRow's flexDirection): without it a scrollable in a
|
|
90
|
+
// flex parent grows to its content height and overflows instead of scrolling.
|
|
91
|
+
const SCROLLABLE_DEFAULTS = { flexShrink: 1 } as const
|
|
92
|
+
const makeScrollable = (style: any, children: any) => new ScrollableElement("scrollable", style, children)
|
|
93
|
+
|
|
94
|
+
/** @internal Compiler fast path (chisel `flatten_ui` raw lowering) — see UIContainer. */
|
|
95
|
+
export const __UIScrollable = (children: UINodeChild[] | ChildrenFn): UIScrollable => makeScrollable({ ...SCROLLABLE_DEFAULTS }, children)
|
|
96
|
+
|
|
97
|
+
/** Create a scroll container (same argument forms as `UIColumn`). As a screen body, pair with
|
|
98
|
+
* `flexGrow: 1`. */
|
|
99
|
+
export function UIScrollable(...children: UIChildArg[]): UIScrollable
|
|
80
100
|
export function UIScrollable(children: UINodeChild[] | ChildrenFn): UIScrollable
|
|
81
|
-
|
|
82
|
-
export function UIScrollable(style: UIScrollableStyle, children
|
|
83
|
-
export function UIScrollable(...args: []
|
|
84
|
-
|
|
85
|
-
// flex parent grows to its content height and overflows instead of scrolling.
|
|
86
|
-
if (args.length === 0) {
|
|
87
|
-
return new ScrollableElement("scrollable", { flexShrink: 1 }, [])
|
|
88
|
-
} else if (args.length === 1) {
|
|
89
|
-
if (Array.isArray(args[0]) || typeof args[0] === "function") {
|
|
90
|
-
return new ScrollableElement("scrollable", { flexShrink: 1 }, args[0])
|
|
91
|
-
} else {
|
|
92
|
-
return new ScrollableElement("scrollable", { flexShrink: 1, ...args[0] }, [])
|
|
93
|
-
}
|
|
94
|
-
} else {
|
|
95
|
-
return new ScrollableElement("scrollable", { flexShrink: 1, ...args[0] }, args[1])
|
|
96
|
-
}
|
|
101
|
+
/** @deprecated Style as the first argument is legacy — use chained `.style({...})`. */
|
|
102
|
+
export function UIScrollable(style: UIScrollableStyle, children?: UINodeChild[] | ChildrenFn): UIScrollable
|
|
103
|
+
export function UIScrollable(...args: any[]): UIScrollable {
|
|
104
|
+
return buildUI(args, SCROLLABLE_DEFAULTS, makeScrollable)
|
|
97
105
|
}
|
|
@@ -2,6 +2,7 @@ import { Element, type ElementStyle, type Classes, type Style, type StyleFn } fr
|
|
|
2
2
|
|
|
3
3
|
export type UISpacerStyle = ElementStyle
|
|
4
4
|
|
|
5
|
+
/** A flexible gap — `flexGrow: 1` by default, pushes its siblings apart. */
|
|
5
6
|
export interface UISpacer {
|
|
6
7
|
readonly type: "spacer",
|
|
7
8
|
style: Style<this, UISpacerStyle>,
|
|
@@ -11,6 +12,7 @@ export interface UISpacer {
|
|
|
11
12
|
* Classes cascade to descendants. See {@link Classes}. */
|
|
12
13
|
readonly class: Classes<this>
|
|
13
14
|
}
|
|
15
|
+
/** Create a flexible gap (`flexGrow: 1`) — e.g. between a header's title and its actions. */
|
|
14
16
|
export function UISpacer(): UISpacer
|
|
15
17
|
export function UISpacer(style: UISpacerStyle): UISpacer
|
|
16
18
|
export function UISpacer(style?: UISpacerStyle): UISpacer {
|