lecodes-sdk 1.2.0 → 2.0.1

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 (260) hide show
  1. package/README.md +5 -2
  2. package/dist/editor.d.ts +12 -0
  3. package/dist/global.d.ts +4 -8
  4. package/dist/host.d.ts +2 -3
  5. package/dist/types/animate/tween/Animation.d.ts +0 -3
  6. package/dist/types/animate/tween/animateValue.d.ts +4 -2
  7. package/dist/types/animate/tween/easing.d.ts +8 -0
  8. package/dist/types/animate/tween/spec.d.ts +15 -7
  9. package/dist/types/audio/audio.d.ts +2 -1
  10. package/dist/types/canvas/Canvas.d.ts +40 -108
  11. package/dist/types/canvas/gen/cssColor.d.ts +17 -0
  12. package/dist/types/canvas/gen/recorder.d.ts +118 -0
  13. package/dist/types/canvas/gen/spec.d.ts +144 -0
  14. package/dist/types/core/color.d.ts +3 -1
  15. package/dist/types/core/pins.d.ts +18 -0
  16. package/dist/types/g2/Node2D.d.ts +5 -8
  17. package/dist/types/g2/Scene2D.d.ts +6 -2
  18. package/dist/types/gl/Foliage.d.ts +30 -7
  19. package/dist/types/gl/Light.d.ts +8 -0
  20. package/dist/types/gl/Lightmap.d.ts +13 -2
  21. package/dist/types/gl/Material.d.ts +17 -3
  22. package/dist/types/gl/Model.d.ts +6 -2
  23. package/dist/types/gl/Node.d.ts +3 -6
  24. package/dist/types/gl/Scene.d.ts +41 -13
  25. package/dist/types/gl/Texture.d.ts +1 -1
  26. package/dist/types/gl/animation/Locomotion.d.ts +8 -1
  27. package/dist/types/inject.d.ts +11 -11
  28. package/dist/types/inject.editor.d.ts +1 -0
  29. package/dist/types/net/core.d.ts +7 -0
  30. package/dist/types/plugin.d.ts +76 -0
  31. package/dist/types/plugins/gen/camera/sdk/camera.d.ts +24 -0
  32. package/dist/types/plugins/gen/camera/sdk/camera.gen.d.ts +25 -0
  33. package/dist/types/plugins/{geolocation.d.ts → gen/geolocation/sdk/geolocation.d.ts} +2 -20
  34. package/dist/types/plugins/gen/geolocation/sdk/geolocation.gen.d.ts +31 -0
  35. package/dist/types/plugins/{map.d.ts → gen/map/sdk/map.d.ts} +9 -50
  36. package/dist/types/plugins/gen/map/sdk/map.gen.d.ts +53 -0
  37. package/dist/types/plugins/gen/push/sdk/push.d.ts +23 -0
  38. package/dist/types/plugins/gen/push/sdk/push.gen.d.ts +35 -0
  39. package/dist/types/plugins/{qr.d.ts → gen/qr-scanner/sdk/qr-scanner.d.ts} +2 -3
  40. package/dist/types/plugins/gen/qr-scanner/sdk/qr-scanner.gen.d.ts +15 -0
  41. package/dist/types/runtime/app.d.ts +9 -2
  42. package/dist/types/runtime/fetch.d.ts +2 -0
  43. package/dist/types/runtime/input.d.ts +1 -1
  44. package/dist/types/runtime/media.d.ts +6 -10
  45. package/dist/types/runtime/misc.d.ts +4 -1
  46. package/dist/types/runtime/net.d.ts +3 -2
  47. package/dist/types/runtime/touch.d.ts +32 -0
  48. package/dist/types/scene/defineScene.d.ts +43 -2
  49. package/dist/types/scene/editor.d.ts +52 -0
  50. package/dist/types/scene/gizmos.d.ts +7 -4
  51. package/dist/types/ui/NativeView.d.ts +6 -4
  52. package/dist/types/ui/UI.d.ts +1 -1
  53. package/dist/types/ui/UIBottomSheet.d.ts +6 -12
  54. package/dist/types/ui/UIButton.d.ts +12 -14
  55. package/dist/types/ui/UIContainer.d.ts +0 -6
  56. package/dist/types/ui/UIImage.d.ts +1 -4
  57. package/dist/types/ui/UIInput.d.ts +8 -24
  58. package/dist/types/ui/UIModal.d.ts +0 -2
  59. package/dist/types/ui/UINode.d.ts +70 -56
  60. package/dist/types/ui/UIPager.d.ts +28 -27
  61. package/dist/types/ui/UIPopover.d.ts +0 -2
  62. package/dist/types/ui/UIScreen.d.ts +12 -17
  63. package/dist/types/ui/UIScrollable.d.ts +1 -4
  64. package/dist/types/ui/UIText.d.ts +0 -2
  65. package/dist/types/ui/UIVideo.d.ts +3 -5
  66. package/dist/types/ui/UIVirtualizedList.d.ts +14 -16
  67. package/dist/types/ui/UIWidget.d.ts +10 -9
  68. package/dist/types/ui/colorKeys.gen.d.ts +9 -0
  69. package/dist/types/ui/presentable.d.ts +46 -32
  70. package/dist/types/ui/router.d.ts +18 -7
  71. package/dist/types/ui/styleColor.d.ts +1 -0
  72. package/dist/types/ui/transitions.d.ts +18 -0
  73. package/dist/types/ui/tree.d.ts +75 -0
  74. package/dist/types/version.d.ts +10 -0
  75. package/dist/types.json +1 -1
  76. package/package.json +12 -3
  77. package/prompts/2d.md +2 -6
  78. package/prompts/3d.md +1 -5
  79. package/prompts/README.md +1 -1
  80. package/prompts/canvas.md +9 -8
  81. package/prompts/compose.ts +1 -1
  82. package/prompts/core.md +3 -3
  83. package/prompts/design.md +1 -1
  84. package/prompts/dist/2d-game.md +473 -239
  85. package/prompts/dist/3d-app.md +553 -205
  86. package/prompts/dist/ar-app.md +435 -202
  87. package/prompts/dist/design.md +113 -95
  88. package/prompts/dist/ui-app.md +386 -170
  89. package/prompts/ui-design.md +2 -3
  90. package/prompts/ui.md +45 -37
  91. package/src/animate/tween/Animation.ts +34 -150
  92. package/src/animate/tween/Timeline.ts +175 -175
  93. package/src/animate/tween/animateValue.ts +6 -3
  94. package/src/animate/tween/easing.ts +10 -3
  95. package/src/animate/tween/spec.ts +41 -15
  96. package/src/audio/Sound.ts +3 -3
  97. package/src/audio/audio.ts +2 -1
  98. package/src/bridges/2d.d.ts +317 -0
  99. package/src/bridges/app.d.ts +91 -0
  100. package/src/bridges/audio.d.ts +97 -0
  101. package/src/bridges/canvas.d.ts +79 -0
  102. package/src/bridges/device.d.ts +72 -0
  103. package/src/bridges/fetch.d.ts +80 -0
  104. package/src/bridges/files.d.ts +70 -0
  105. package/src/bridges/gl.d.ts +1133 -0
  106. package/src/bridges/input.d.ts +72 -0
  107. package/src/bridges/media.d.ts +55 -0
  108. package/src/bridges/nav.d.ts +71 -0
  109. package/src/bridges/net.d.ts +52 -0
  110. package/src/bridges/service.d.ts +52 -0
  111. package/src/bridges/socket.d.ts +31 -0
  112. package/src/bridges/storage.d.ts +33 -0
  113. package/src/bridges/tree.d.ts +301 -0
  114. package/src/bridges/types.d.ts +49 -0
  115. package/src/canvas/Canvas.ts +114 -159
  116. package/src/canvas/gen/cssColor.ts +224 -0
  117. package/src/canvas/gen/recorder.ts +212 -0
  118. package/src/canvas/gen/spec.ts +201 -0
  119. package/src/chisel.ts +193 -0
  120. package/src/compile/assetMacro.ts +1 -1
  121. package/src/compile/bundler.ts +11 -2
  122. package/src/compile/compileProject.ts +43 -4
  123. package/src/compile/fontMacro.ts +3 -4
  124. package/src/compile/header.ts +26 -5
  125. package/src/compile/index.ts +3 -1
  126. package/src/compile/liteMaterial.ts +1 -1
  127. package/src/compile/sceneEditor.ts +11 -26
  128. package/src/core/color.ts +73 -30
  129. package/src/core/pins.ts +51 -0
  130. package/src/core/signals.ts +8 -1
  131. package/src/g2/CharacterController2D.ts +3 -3
  132. package/src/g2/Node2D.ts +57 -39
  133. package/src/g2/Physics2D.ts +2 -2
  134. package/src/g2/Scene2D.ts +35 -23
  135. package/src/g2/Texture2D.ts +1 -1
  136. package/src/g2/loop.ts +4 -4
  137. package/src/gl/CameraPlace.ts +52 -52
  138. package/src/gl/Foliage.ts +72 -17
  139. package/src/gl/Geometry.ts +1 -2
  140. package/src/gl/Light.ts +10 -0
  141. package/src/gl/Lightmap.ts +45 -29
  142. package/src/gl/Material.ts +95 -51
  143. package/src/gl/Mesh.ts +120 -120
  144. package/src/gl/Model.ts +21 -16
  145. package/src/gl/Node.ts +91 -24
  146. package/src/gl/Particles.ts +1 -1
  147. package/src/gl/Scene.ts +100 -44
  148. package/src/gl/Texture.ts +8 -7
  149. package/src/gl/animation/AnimationClip.ts +1 -1
  150. package/src/gl/animation/DynamicBone.ts +482 -482
  151. package/src/gl/animation/Locomotion.ts +8 -3
  152. package/src/gl/nav/NavMesh.ts +3 -4
  153. package/src/gl/physics/Physics.ts +2 -2
  154. package/src/gl/physics/physicsEvents.ts +3 -3
  155. package/src/gl/scenarios.ts +291 -291
  156. package/src/gl/terrain/Terrain.ts +4 -5
  157. package/src/gl/touch.ts +14 -15
  158. package/src/host.d.ts +2 -3
  159. package/src/inject.editor.ts +7 -0
  160. package/src/inject.ts +13 -16
  161. package/src/net/core.ts +6 -5
  162. package/src/net/index.ts +1 -1
  163. package/src/net/replication.ts +1 -1
  164. package/src/plugin.ts +191 -0
  165. package/src/plugins/gen/camera/contract.d.ts +27 -0
  166. package/src/plugins/gen/camera/sdk/camera.gen.ts +46 -0
  167. package/src/plugins/gen/camera/sdk/camera.ts +57 -0
  168. package/src/plugins/gen/geolocation/contract.d.ts +50 -0
  169. package/src/plugins/gen/geolocation/sdk/geolocation.gen.ts +54 -0
  170. package/src/plugins/{geolocation.ts → gen/geolocation/sdk/geolocation.ts} +22 -43
  171. package/src/plugins/gen/map/contract.d.ts +144 -0
  172. package/src/plugins/gen/map/sdk/map.gen.ts +88 -0
  173. package/src/plugins/{map.ts → gen/map/sdk/map.ts} +68 -102
  174. package/src/plugins/gen/push/contract.d.ts +61 -0
  175. package/src/plugins/gen/push/sdk/push.gen.ts +60 -0
  176. package/src/plugins/gen/push/sdk/push.ts +105 -0
  177. package/src/plugins/gen/qr-scanner/contract.d.ts +16 -0
  178. package/src/plugins/gen/qr-scanner/sdk/qr-scanner.gen.ts +29 -0
  179. package/src/plugins/gen/qr-scanner/sdk/qr-scanner.ts +52 -0
  180. package/src/plugins/permission.ts +5 -4
  181. package/src/runtime/app.ts +20 -9
  182. package/src/runtime/appEvents.ts +5 -4
  183. package/src/runtime/channel.ts +18 -15
  184. package/src/runtime/clipboard.ts +4 -3
  185. package/src/runtime/datetime.ts +2 -1
  186. package/src/runtime/device.ts +17 -15
  187. package/src/runtime/fetch.ts +30 -20
  188. package/src/runtime/files.ts +16 -15
  189. package/src/runtime/input.ts +12 -10
  190. package/src/runtime/media.ts +50 -46
  191. package/src/runtime/misc.ts +7 -3
  192. package/src/runtime/net.ts +8 -7
  193. package/src/runtime/rpc.ts +1 -3
  194. package/src/runtime/service.ts +19 -14
  195. package/src/runtime/share.ts +4 -3
  196. package/src/runtime/storage.ts +6 -4
  197. package/src/runtime/touch.ts +32 -0
  198. package/src/scene/defineScene.ts +61 -365
  199. package/src/scene/editor.ts +408 -0
  200. package/src/scene/editorPlugins.ts +3 -3
  201. package/src/scene/gizmos.ts +15 -9
  202. package/src/server/db/marci/query.ts +1 -1
  203. package/src/server/host.ts +1 -1
  204. package/src/server/runtime.ts +1 -1
  205. package/src/ui/NativeView.ts +72 -25
  206. package/src/ui/UI.ts +3 -3
  207. package/src/ui/UIBottomSheet.ts +16 -17
  208. package/src/ui/UIButton.ts +54 -16
  209. package/src/ui/UIContainer.ts +0 -6
  210. package/src/ui/UIImage.ts +34 -30
  211. package/src/ui/UIInput.ts +29 -37
  212. package/src/ui/UIModal.ts +1 -3
  213. package/src/ui/UINode.ts +347 -297
  214. package/src/ui/UIPager.ts +93 -78
  215. package/src/ui/UIPopover.ts +0 -2
  216. package/src/ui/UIScreen.ts +41 -36
  217. package/src/ui/UIScrollable.ts +19 -13
  218. package/src/ui/UISpacer.ts +1 -1
  219. package/src/ui/UITabs.ts +8 -6
  220. package/src/ui/UIText.ts +7 -17
  221. package/src/ui/UIVideo.ts +30 -27
  222. package/src/ui/UIVirtualizedList.ts +58 -59
  223. package/src/ui/UIWidget.ts +38 -18
  224. package/src/ui/colorKeys.gen.ts +37 -0
  225. package/src/ui/fonts.ts +2 -2
  226. package/src/ui/presentable.ts +59 -42
  227. package/src/ui/router.ts +52 -35
  228. package/src/ui/styleColor.ts +56 -0
  229. package/src/ui/theme.ts +7 -6
  230. package/src/ui/transitions.ts +249 -0
  231. package/src/ui/tree.ts +346 -0
  232. package/src/version.ts +24 -0
  233. package/tests/helpers/engineWorld.ts +11 -0
  234. package/tests/helpers/fakeTree.ts +353 -0
  235. package/tests/helpers/hostStubs.ts +31 -0
  236. package/tests/helpers/index.ts +14 -0
  237. package/tests/helpers/memoryMarci.ts +124 -0
  238. package/tests/helpers/phases.ts +23 -0
  239. package/tests/helpers/preload.ts +18 -0
  240. package/tests/helpers/stubApp.ts +2 -0
  241. package/tests/helpers/stubDevice.ts +2 -0
  242. package/tests/helpers/stubFetch.ts +2 -0
  243. package/tests/helpers/stubInput.ts +2 -0
  244. package/dist/inject.js +0 -4629
  245. package/dist/types/core/registry.d.ts +0 -7
  246. package/dist/types/plugins/camera.d.ts +0 -25
  247. package/dist/types/plugins/push.d.ts +0 -46
  248. package/src/bridges.d.ts +0 -1769
  249. package/src/compile/__tests__/assetIconMacro.test.ts +0 -219
  250. package/src/compile/__tests__/assetMacro.test.ts +0 -100
  251. package/src/compile/__tests__/assetName.test.ts +0 -55
  252. package/src/compile/__tests__/compile.test.ts +0 -310
  253. package/src/compile/__tests__/detectEntry.test.ts +0 -151
  254. package/src/compile/__tests__/fontMacro.test.ts +0 -199
  255. package/src/compile/__tests__/serverSplit.test.ts +0 -27
  256. package/src/core/__tests__/stateMachine.test.ts +0 -132
  257. package/src/core/registry.ts +0 -23
  258. package/src/plugins/camera.ts +0 -81
  259. package/src/plugins/push.ts +0 -132
  260. package/src/plugins/qr.ts +0 -73
package/src/ui/UIPager.ts CHANGED
@@ -1,6 +1,9 @@
1
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
+ import type { NavigateOptions, PopOptions } from "./router"
5
+ import { _transitionBack, _transitionId, TRANSITION_UNSET } from "./transitions"
6
+ import { pin, tree } from "./tree"
4
7
 
5
8
  // UIPager — the system's ONE screen-navigation element (docs/pager-component-plan.md): sibling
6
9
  // tabs that swipe natively side to side, with a native navigation STACK per tab. `push` opens a
@@ -11,12 +14,15 @@ import type { UIScreen } from "./UIScreen"
11
14
  // the current screen (UIPager.current) — no reference threading into child screens. Router stays
12
15
  // distinct: `Router.push` opens ON TOP of the pager (bars and all), the pager navigates within.
13
16
  //
14
- // Wire node type "pager": children are separate layout roots (full UIScreens), NOT yoga children.
15
- // The wire shape is `children` flat in TAB-MAJOR order (tab 0's stack root→top, then tab 1's, …)
16
- // partitioned by `_lens` (per-tab stack lengths) with `_index` = selected tab. State is mutated
17
- // BEFORE each command, so the host reconciles against the node; commands only name the native
18
- // transition to play. Host-driven changes come back via `_hostDidSelect` / `_hostDidPop` — both
19
- // idempotent no-ops when the logical state already matches.
17
+ // The SDK owns the logical state (`_pages` flat in TAB-MAJOR order, `_lens` = per-tab stack
18
+ // lengths, `_index` = the selected tab) and sends it to the runtime whole (`pagerSetContent`) or
19
+ // as a stack command with the page id; the runtime mirrors it, parents the pages' views under the
20
+ // pager and reports host-driven changes back (TREE_EVENT_PAGER_SELECT / PAGER_POP) — both
21
+ // idempotent no-ops here when the logical state already matches.
22
+ //
23
+ // A change of a tab's stack plays a transition exactly as the Router's does (transitions.ts): the
24
+ // same names and specs, the same way back remembered by the entry — the runtime plays it between
25
+ // the two pages, inside the pager's box.
20
26
 
21
27
  /** Styles a pager accepts — layout box + drawable (background, borders, …). Pages style
22
28
  * themselves; the pager only owns its own slot. */
@@ -59,17 +65,21 @@ export interface UIPager extends UIElementBase<UIPagerStyle, DrawableStyle & Bas
59
65
  /** Fires when the selected tab changes and has settled — a user swipe or `select()`. */
60
66
  onSelect(callback: (index: number) => void): this,
61
67
 
62
- /** Push `screen` on top of the **current tab** — slides in natively; the edge back-swipe (or
63
- * `pop()`) removes it. Other tabs are unaffected. */
64
- push(screen: UIScreen): this,
68
+ /** Push `screen` on top of the **current tab**; the edge back-swipe (or `pop()`) removes it.
69
+ * Other tabs are unaffected. Transitions as in `Router.push`: `"push"` unless one is named, and
70
+ * the screen remembers its way back (`popTransition`, or the mirror of the way it came). */
71
+ push(screen: UIScreen, opts?: NavigateOptions): this,
65
72
  /** Pop the current tab's top screen (no-op at the tab root — the root never pops). The
66
- * programmatic twin of the native edge back-swipe. */
67
- pop(): this,
68
- /** Unwind the current tab back to its root screen in one step. */
69
- popToRoot(): this,
73
+ * programmatic twin of the native edge back-swipe. Plays the way back the screen remembers
74
+ * unless a transition is named. */
75
+ pop(opts?: PopOptions): this,
76
+ /** Unwind the current tab back to its root screen in one step: the top screen leaves with its
77
+ * way back (or the named transition), the ones between are never shown. */
78
+ popToRoot(opts?: PopOptions): this,
70
79
  /** Swap the current tab's top screen for `screen` without growing the stack (at depth 1 this
71
- * swaps the tab's root). */
72
- replace(screen: UIScreen): this,
80
+ * swaps the tab's root). `"fade"` unless a transition is named; the way back stays the one of
81
+ * the place in the stack unless `popTransition` names another. */
82
+ replace(screen: UIScreen, opts?: NavigateOptions): this,
73
83
  /** The current tab's screens, root → top (read-only snapshot). */
74
84
  readonly stack: UIScreen[],
75
85
  /** The current tab's stack depth — 1 means only the root. While `depth > 1`, swiping between
@@ -77,13 +87,16 @@ export interface UIPager extends UIElementBase<UIPagerStyle, DrawableStyle & Bas
77
87
  readonly depth: number,
78
88
  /** Fires when the current tab's depth changes — push, pop, replace, or a back-swipe. */
79
89
  onChange(callback: (depth: number) => void): this,
90
+ /** The pages, tab-major (every tab's stack root → top). */
91
+ readonly children: UIScreen[],
80
92
  }
81
93
 
82
94
  export class PagerElement extends ContainerElement<"pager"> {
83
- /** Per-tab stack lengths — partitions the flat tab-major `children`. Named `_*` so chisel
84
- * keeps it (the host reads it live). */
85
- _lens: number[]
86
- /** Selected tab. */
95
+ /** @internal The pages, flat in tab-major order. */
96
+ _pages: any[] = []
97
+ /** @internal Per-tab stack lengths — partitions `_pages`. */
98
+ _lens: number[] = []
99
+ /** @internal Selected tab. */
87
100
  _index = 0
88
101
  // >0 while a screen owned by this pager is the visible one (its onOpen fired, onClose hasn't).
89
102
  // Only one owned screen is ever open, so this is effectively 0/1 — the count only makes the
@@ -96,24 +109,33 @@ export class PagerElement extends ContainerElement<"pager"> {
96
109
  // flexShrink: 1 default (SDK-owned, like UIScrollable's): pages are separate layout roots
97
110
  // sized to the pager's box, so yoga sees no shrinkable content — without this a pager in a
98
111
  // flex column overflows the screen instead of leaving room for the tab bar.
99
- super("pager", { flexShrink: 1, ...style }, tabs as any)
100
- this._lens = tabs.map(() => 1)
101
- for (const s of this.children) this._track(s)
112
+ super("pager", { flexShrink: 1, ...style }, [])
113
+ this._rebuild(tabs as any[])
102
114
  }
103
115
 
104
- /** Start offset of tab `i`'s segment in the flat `children`. */
116
+ /** Start offset of tab `i`'s segment in the flat `_pages`. */
105
117
  private _offset(i: number): number {
106
118
  let off = 0
107
119
  for (let t = 0; t < i; t++) off += this._lens[t]
108
120
  return off
109
121
  }
110
122
 
123
+ /** Push the whole picture to the runtime. */
124
+ private _sync(): void {
125
+ if (this._h.id === 0) return
126
+ for (const p of this._pages) pin(p)
127
+ tree().pagerSetContent(this._h.id, this._pages.map((p) => p._h.id), this._lens.slice(), this._index)
128
+ }
129
+
130
+ get children(): any[] {
131
+ return this._pages.slice()
132
+ }
111
133
  get index(): number {
112
134
  return this._index
113
135
  }
114
136
  get stack(): UIScreen[] {
115
137
  const off = this._offset(this._index)
116
- return this.children.slice(off, off + (this._lens[this._index] ?? 0))
138
+ return this._pages.slice(off, off + (this._lens[this._index] ?? 0))
117
139
  }
118
140
  get depth(): number {
119
141
  return this._lens[this._index] ?? 0
@@ -131,74 +153,78 @@ export class PagerElement extends ContainerElement<"pager"> {
131
153
  select(index: number, animated = false): this {
132
154
  if (index < 0 || index >= this._lens.length || index === this._index) return this
133
155
  this._index = index
134
- if (this._id !== 0) _creatorUI.command(this, "select", animated)
156
+ if (this._h.id !== 0) tree().pagerSelect(this._h.id, index, animated)
135
157
  this._emitSelect()
136
158
  return this
137
159
  }
138
160
 
139
- push(screen: UIScreen): this {
161
+ push(screen: UIScreen, opts?: NavigateOptions): this {
140
162
  if (this._lens.length === 0) {
141
163
  console.warn("UIPager.push: pager has no tabs")
142
164
  return this
143
165
  }
144
- this.children.splice(this._offset(this._index) + this._lens[this._index], 0, screen)
166
+ this._pages.splice(this._offset(this._index) + this._lens[this._index], 0, screen)
145
167
  this._lens[this._index]++
146
168
  this._track(screen)
147
- if (this._id !== 0) _creatorUI.command(this, "push")
169
+ pin(screen as any)
170
+ if (this._h.id !== 0) {
171
+ const transition = opts?.transition ?? "push"
172
+ const back = opts?.popTransition !== undefined ? _transitionId(opts.popTransition) : _transitionBack(transition)
173
+ tree().pagerPush(this._h.id, (screen as any)._h.id, _transitionId(transition), back)
174
+ }
148
175
  this._emitChange()
149
176
  return this
150
177
  }
151
178
 
152
- pop(): this {
179
+ pop(opts?: PopOptions): this {
153
180
  if ((this._lens[this._index] ?? 0) <= 1) return this
154
- this.children.splice(this._offset(this._index) + this._lens[this._index] - 1, 1)
181
+ this._pages.splice(this._offset(this._index) + this._lens[this._index] - 1, 1)
155
182
  this._lens[this._index]--
156
- if (this._id !== 0) _creatorUI.command(this, "pop")
183
+ // the runtime detaches the page when its transition lands (its close fires first)
184
+ if (this._h.id !== 0) tree().pagerPop(this._h.id, _transitionId(opts?.transition, TRANSITION_UNSET))
157
185
  this._emitChange()
158
186
  return this
159
187
  }
160
188
 
161
- popToRoot(): this {
189
+ popToRoot(opts?: PopOptions): this {
162
190
  const len = this._lens[this._index] ?? 0
163
191
  if (len <= 1) return this
164
- this.children.splice(this._offset(this._index) + 1, len - 1)
192
+ this._pages.splice(this._offset(this._index) + 1, len - 1)
165
193
  this._lens[this._index] = 1
166
- if (this._id !== 0) _creatorUI.command(this, "popToRoot")
194
+ if (this._h.id !== 0) tree().pagerPopToRoot(this._h.id, _transitionId(opts?.transition, TRANSITION_UNSET))
167
195
  this._emitChange()
168
196
  return this
169
197
  }
170
198
 
171
- replace(screen: UIScreen): this {
199
+ replace(screen: UIScreen, opts?: NavigateOptions): this {
172
200
  if (this._lens.length === 0) {
173
201
  console.warn("UIPager.replace: pager has no tabs")
174
202
  return this
175
203
  }
176
- this.children[this._offset(this._index) + this._lens[this._index] - 1] = screen
204
+ const at = this._offset(this._index) + this._lens[this._index] - 1
205
+ const old = this._pages[at]
206
+ if (old === screen) return this
207
+ this._pages[at] = screen
177
208
  this._track(screen)
178
- if (this._id !== 0) _creatorUI.command(this, "replace")
209
+ pin(screen as any)
210
+ if (this._h.id !== 0) tree().pagerReplace(this._h.id, (screen as any)._h.id, _transitionId(opts?.transition ?? "fade"), _transitionId(opts?.popTransition))
179
211
  this._emitChange()
180
212
  return this
181
213
  }
182
214
 
183
215
  // ── Children ARE the tabs ──────────────────────────────────────────────────────────────────
184
- // Every inherited container mutation would send a generic setContent/insertNode/removeNode wire
185
- // op, and a host must never run those on a pager: its children are page ROOTS the host owns
186
- // (each laid out independently at the pager's box), not yoga children — the generic path frees
187
- // the live pages and rebuilds them as ordinary children stacked on the first tab. So each one is
188
- // overridden to mutate the element first (children, `_lens`, `_index`) and then notify over the
189
- // same command channel every other pager op uses, where the host reconciles against the node.
190
- //
191
- // Content set this way is always one screen per tab: a per-tab STACK only ever grows through
192
- // push/replace, so `_lens` resets to all-ones here.
216
+ // Every inherited container mutation is overridden to mutate the logical state first and then
217
+ // hand the runtime the whole picture (pagerSetContent), where it reconciles the pages. Content set
218
+ // this way is always one screen per tab: a per-tab STACK only ever grows through push/replace,
219
+ // so `_lens` resets to all-ones here.
193
220
  private _rebuild(children: any[]): this {
194
221
  const tabs = children.filter(Boolean)
195
222
  const prevIndex = this._index
196
- this.children.length = 0
197
- this.children.push(...tabs)
223
+ this._pages = tabs
198
224
  this._lens = tabs.map(() => 1)
199
225
  if (this._index >= tabs.length) this._index = Math.max(0, tabs.length - 1)
200
226
  for (const s of tabs) this._track(s)
201
- if (this._id !== 0) _creatorUI.command(this, "content")
227
+ this._sync()
202
228
  if (this._index !== prevIndex) this._emitSelect()
203
229
  this._emitChange()
204
230
  return this
@@ -213,36 +239,36 @@ export class PagerElement extends ContainerElement<"pager"> {
213
239
  return this._rebuild(children as any[])
214
240
  }
215
241
  append(...nodes: UINodeChild[]): this {
216
- return this._rebuild([...this.children, ...nodes])
242
+ return this._rebuild([...this._pages, ...nodes])
217
243
  }
218
244
  insert(index: number, ...nodes: UINodeChild[]): this {
219
- const next = this.children.slice()
245
+ const next = this._pages.slice()
220
246
  next.splice(index, 0, ...(nodes as any[]))
221
247
  return this._rebuild(next)
222
248
  }
223
- remove(...nodesToDelete: UIScreen[]): this {
249
+ remove(...nodesToDelete: UINodeChild[]): this {
224
250
  const drop = new Set<any>(nodesToDelete)
225
- return this._rebuild(this.children.filter((c: any) => !drop.has(c)))
251
+ return this._rebuild(this._pages.filter((c: any) => !drop.has(c)))
226
252
  }
227
253
  _reconcile(next: UINodeChild[]): void {
228
254
  this._rebuild(next as any[])
229
255
  }
230
256
 
231
- // Host-driven tab change (committed swipe): the native pager already moved — sync the logical
232
- // index + fire onSelect. Named `_*` so chisel keeps it (hosts call it).
257
+ /** @internal Host-driven tab change (committed swipe): the native pager already moved — sync
258
+ * the logical index + fire onSelect. */
233
259
  _hostDidSelect(index: number): void {
234
260
  if (index < 0 || index >= this._lens.length || index === this._index) return
235
261
  this._index = index
236
262
  this._emitSelect()
237
263
  }
238
264
 
239
- // Host-driven pop on the current tab (interactive edge back-swipe): the native stack already
240
- // popped — sync the logical stack + fire onChange. Idempotent, safe after every transition.
265
+ /** @internal Host-driven pop on the current tab (interactive edge back-swipe / back button): the
266
+ * native stack already popped — sync the logical stack + fire onChange. Idempotent. */
241
267
  _hostDidPop(depth: number): void {
242
268
  if (depth < 1) depth = 1
243
269
  const len = this._lens[this._index] ?? 0
244
270
  if (depth >= len) return
245
- this.children.splice(this._offset(this._index) + depth, len - depth)
271
+ this._pages.splice(this._offset(this._index) + depth, len - depth)
246
272
  this._lens[this._index] = depth
247
273
  this._emitChange()
248
274
  }
@@ -281,8 +307,6 @@ export class PagerElement extends ContainerElement<"pager"> {
281
307
  }
282
308
  }
283
309
 
284
- const isScreenArg = (v: any): boolean => v != null && typeof v === "object" && v.type === "screen"
285
-
286
310
  /** Callable factory + ambient statics. The name `UIPager` is a value (this factory) and a type
287
311
  * (the instance interface above) — a standard value/type merge. */
288
312
  interface UIPagerConstructor {
@@ -291,32 +315,23 @@ interface UIPagerConstructor {
291
315
  * swiping); several → swipeable sibling tabs, fixed at construction, each starting as its own
292
316
  * one-screen stack. */
293
317
  (...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
296
318
  /** The pager that owns the currently visible screen, or `null` when the visible screen isn't
297
319
  * hosted by one. With pagers nested inside pages, resolves to the **innermost** — so ambient
298
320
  * calls act on the pager the user is actually looking at. */
299
321
  readonly current: UIPager | null
300
322
  /** `UIPager.current.push(screen)` — push onto the pager owning the visible screen, no
301
323
  * reference threading. No-op with a console warning when there is no current pager. */
302
- push(screen: UIScreen): void
324
+ push(screen: UIScreen, opts?: NavigateOptions): void
303
325
  /** `UIPager.current.pop()` — pop the visible pager's current tab (no-op at the tab root, or
304
326
  * with a warning when there is no current pager). */
305
- pop(): void
327
+ pop(opts?: PopOptions): void
306
328
  /** `UIPager.current.popToRoot()` — unwind the visible pager's current tab to its root. */
307
- popToRoot(): void
329
+ popToRoot(opts?: PopOptions): void
308
330
  }
309
331
 
310
332
  function pagerFactory(...args: any[]): 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
333
  const tabs: any[] = []
319
- for (let i = start; i < args.length; i++) {
334
+ for (let i = 0; i < args.length; i++) {
320
335
  const a = args[i]
321
336
  if (Array.isArray(a)) {
322
337
  for (let j = 0; j < a.length; j++) if (a[j]) tabs.push(a[j])
@@ -324,7 +339,7 @@ function pagerFactory(...args: any[]): UIPager {
324
339
  tabs.push(a)
325
340
  }
326
341
  }
327
- return new PagerElement(style, tabs) as unknown as UIPager
342
+ return new PagerElement(null, tabs) as unknown as UIPager
328
343
  }
329
344
 
330
345
  /**
@@ -351,12 +366,12 @@ const _resolveCurrent = (): UIPager | null => {
351
366
  // as part of that binding's declaration (`ItemKind::AttachedTo`, chisel >= 0.4.2) — kept iff
352
367
  // `UIPager` is reached, shaken away with it otherwise. Under chisel <= 0.4.1 they were dropped
353
368
  // outright and `UIPager.push` arrived as `undefined` at runtime, in bundled builds only; don't lower
354
- // the pinned `@letary/chisel` back past 0.4.2. Covered by sdk/tests/compile.test.ts.
369
+ // the pinned `@letary/chisel` back past 0.4.2. Covered by sdk/tests/compile/inject.test.ts.
355
370
  // `Object.defineProperty` for `current`, not `Object.assign({ get current() {} })` — Object.assign
356
371
  // *invokes* the getter and copies the resulting value, freezing it at null forever.
357
372
  Object.defineProperty(UIPager, "current", {
358
373
  get: () => (_activeStack[_activeStack.length - 1] ?? null) as unknown as UIPager | null,
359
374
  })
360
- ;(UIPager as any).push = (screen: UIScreen) => { _resolveCurrent()?.push(screen) }
361
- ;(UIPager as any).pop = () => { _resolveCurrent()?.pop() }
362
- ;(UIPager as any).popToRoot = () => { _resolveCurrent()?.popToRoot() }
375
+ ;(UIPager as any).push = (screen: UIScreen, opts?: NavigateOptions) => { _resolveCurrent()?.push(screen, opts) }
376
+ ;(UIPager as any).pop = (opts?: PopOptions) => { _resolveCurrent()?.pop(opts) }
377
+ ;(UIPager as any).popToRoot = (opts?: PopOptions) => { _resolveCurrent()?.popToRoot(opts) }
@@ -93,8 +93,6 @@ const makePopover = (style: any, children: any) => new PopoverElement(style, chi
93
93
  * `show(anchor)` next to an element or `{x, y}` point. Same argument forms as `UIColumn`. */
94
94
  export function UIPopover(...children: UIChildArg[]): UIPopover;
95
95
  export function UIPopover(children: UINodeChild[] | ChildrenFn): UIPopover;
96
- /** @deprecated Style as the first argument is legacy — use chained `.style({...})`. */
97
- export function UIPopover(style: UIPopoverStyle, children?: UINodeChild[] | ChildrenFn): UIPopover;
98
96
  export function UIPopover(...args: any[]): UIPopover {
99
97
  return buildUI(args, null, makePopover)
100
98
  }
@@ -1,6 +1,8 @@
1
1
  import { TouchStartEvent } from "../runtime/touch"
2
- import { _bumpNavEpoch, _navSupported, _setCurrent, Presentable, type PresentOptions } from "./presentable"
2
+ import { _bumpNavEpoch, _setCurrent, Presentable, type DestTuple, type DismissOptions, type PresentOptions } from "./presentable"
3
+ import { _transitionId, TRANSITION_NONE } from "./transitions"
3
4
  import { ContainerElement, buildUI, type BaseStyle, type ContainerStyle, type DrawableStyle, type PaddingStyle, type UIChildArg, type UIContainerBase, type UINodeChild, type ChildrenFn, } from "./UINode"
5
+ import { TREE_DEST_SCREEN, TREE_FLAG_BACK, TREE_FLAG_INTERACTIVE, TREE_FLAG_KEEP_ALIVE, pin, tree } from "./tree"
4
6
 
5
7
  export type UIScreenStyle = ContainerStyle & DrawableStyle & PaddingStyle & BaseStyle
6
8
 
@@ -15,9 +17,12 @@ export interface UIScreen extends UIContainerBase<UIScreenStyle, DrawableStyle &
15
17
  * scene, a native view — suspending an active Router until `Router.restore()`). */
16
18
  open(options?: PresentOptions): void,
17
19
  /** Dismiss if this screen is the visible destination. */
18
- close(): void
20
+ close(options?: DismissOptions): void
19
21
 
20
22
  /** Hardware/system back while this screen is current. */
23
+ onBack(callback: () => void): this
24
+ /** @deprecated Renamed `onBack` (2026-09-26). An alias for the projects compiled before the
25
+ * rename; removed with the release that recompiles them. */
21
26
  onBackPressed(callback: () => void): this
22
27
 
23
28
  /** The screen became the visible destination (first open, or revealed by a pop). */
@@ -26,38 +31,32 @@ export interface UIScreen extends UIContainerBase<UIScreenStyle, DrawableStyle &
26
31
  onClose(callback: () => void): this
27
32
 
28
33
  /**
29
- * Keep this screen's built native tree alive when a host detaches it: instead of being destroyed,
30
- * its views + layout + live state (scroll position, input text) are retained, so re-hosting the
31
- * SAME screen object restores it in place rather than rebuilding. `onClose`/`onOpen` still fire on
32
- * detach/re-attach. You own the lifetime — a kept tree is freed only by `dispose()` or when the
33
- * project unloads. (Native-only today; web rebuilds.)
34
+ * Keep this screen's native state when a pager releases it: instead of dropping its view, the
35
+ * host keeps it (scroll position, input text), so re-pushing the SAME screen object restores it
36
+ * in place. `onClose`/`onOpen` still fire on detach/re-attach. Free it with `destroy()`.
34
37
  */
35
38
  keepAlive(enabled?: boolean): this
36
- /** Free a `keepAlive()` screen's retained tree now. No-op unless it's currently detached-and-kept. */
37
- dispose(): this
38
39
 
39
40
  /** Touch began on the screen; `ev.track(...)` takes over the rest of the gesture. */
40
41
  onTouchStart(callback: (ev: TouchStartEvent<UIScreen>) => void): this,
42
+ /** @internal The destination tuple (Presentable). */
43
+ _dest(): DestTuple
41
44
  }
42
45
 
43
46
  export class ScreenElement extends ContainerElement<"screen"> {
47
+ /** @internal */
48
+ _dest(): DestTuple { return [TREE_DEST_SCREEN, this._h.id, "", ""] }
49
+
44
50
  open(options?: PresentOptions): void {
51
+ if (this._h.id === 0) return
45
52
  _bumpNavEpoch()
46
- if (_navSupported()) {
47
- _creatorUI.openView!(this, options?.transition ?? "none")
48
- } else {
49
- // Legacy host: no destination navigation — transition is dropped.
50
- _creatorUI.openScreen(this)
51
- }
53
+ pin(this) // presented: the runtime holds it until it retires it (TREE_EVENT_DETACHED)
54
+ tree().open(TREE_DEST_SCREEN, this._h.id, "", "", _transitionId(options?.transition, TRANSITION_NONE))
52
55
  _setCurrent(this as any)
53
56
  }
54
- close(): void {
57
+ close(options?: DismissOptions): void {
55
58
  _bumpNavEpoch()
56
- if (_navSupported()) {
57
- _creatorUI.closeView!()
58
- } else {
59
- _creatorUI.closeScreen()
60
- }
59
+ tree().close(TREE_DEST_SCREEN, this._h.id, _transitionId(options?.transition, TRANSITION_NONE))
61
60
  if (Presentable.current === (this as any)) _setCurrent(null)
62
61
  }
63
62
  protected ol: (() => void)[] = []
@@ -70,25 +69,24 @@ export class ScreenElement extends ContainerElement<"screen"> {
70
69
  this.cl.push(callback)
71
70
  return this
72
71
  }
73
- // Read by the host at detach time (a plain flag on the node, like _interactive).
74
- _keepAlive = false
72
+ /** @internal presentation lifecycle from the runtime (also a pager's page open / close). */
73
+ _emitOpen(): void { for (const cb of this.ol.slice()) cb() }
74
+ /** @internal */
75
+ _emitClose(): void { for (const cb of this.cl.slice()) cb() }
76
+
75
77
  keepAlive(enabled = true): this {
76
- this._keepAlive = enabled
77
- return this
78
- }
79
- dispose(): this {
80
- // Rides the generic command channel (no dedicated bridge). No-op while _id is 0 (never mounted)
81
- // or when the tree isn't currently detached-and-kept — the host ignores it then.
82
- if (this._id !== 0) _creatorUI.command(this, "disposeKeepAlive")
78
+ if (enabled) this._addFlags(TREE_FLAG_KEEP_ALIVE)
79
+ else this._removeFlags(TREE_FLAG_KEEP_ALIVE)
83
80
  return this
84
81
  }
82
+
85
83
  readonly touchStartListeners: any[] = []
86
- _interactive = false
87
84
  onTouchStart (callback: any): this {
88
- this._interactive = true
89
85
  this.touchStartListeners.push(callback)
86
+ this._addFlags(TREE_FLAG_INTERACTIVE)
90
87
  return this
91
88
  }
89
+ /** @internal */
92
90
  _emitTouchStart(pointerId: number, clientX: number, clientY: number): any {
93
91
  const touchEvent = new TouchStartEvent(clientX, clientY, pointerId)
94
92
  touchEvent.target = this
@@ -100,11 +98,20 @@ export class ScreenElement extends ContainerElement<"screen"> {
100
98
  }
101
99
  return null
102
100
  }
103
- _backButtonCallback?: () => void
104
- onBackPressed(callback: any): this {
101
+ private _backButtonCallback?: () => void
102
+ onBack(callback: any): this {
105
103
  this._backButtonCallback = callback
104
+ this._addFlags(TREE_FLAG_BACK)
106
105
  return this
107
106
  }
107
+ /** @deprecated see the interface */
108
+ onBackPressed(callback: any): this { return this.onBack(callback) }
109
+ /** @internal the runtime's back chain: true = handled. */
110
+ _emitBack(): boolean {
111
+ if (!this._backButtonCallback) return false
112
+ this._backButtonCallback()
113
+ return true
114
+ }
108
115
  }
109
116
 
110
117
  const makeScreen = (style: any, children: any) => new ScreenElement("screen", style, children)
@@ -116,8 +123,6 @@ export const __UIScreen = (children: UINodeChild[] | ChildrenFn): UIScreen => ma
116
123
  * as `UIColumn`. */
117
124
  export function UIScreen(...children: UIChildArg[]): UIScreen;
118
125
  export function UIScreen(children: UINodeChild[] | ChildrenFn): UIScreen;
119
- /** @deprecated Style as the first argument is legacy — use chained `.style({...})`. */
120
- export function UIScreen(style: UIScreenStyle, children?: UINodeChild[] | ChildrenFn): UIScreen;
121
126
  export function UIScreen(...args: any[]): UIScreen {
122
127
  return buildUI(args, null, makeScreen)
123
128
  }
@@ -1,5 +1,6 @@
1
1
  import type { UIContainerStyle } from "./UIContainer"
2
2
  import { type UIChildArg, type UIContainerBase, type UINodeChild, type ChildrenFn, type Color, ContainerElement, buildUI } from "./UINode"
3
+ import { TREE_FLAG_OVERSCROLL, TREE_FLAG_REFRESH, TREE_FLAG_SCROLL, TREE_FLAG_SCROLL_RELEASE } from "./tree"
3
4
 
4
5
  export type ScrollStyle = {
5
6
  showScrollbar?: boolean,
@@ -30,42 +31,49 @@ export interface UIScrollable extends UIContainerBase<UIScrollableStyle> {
30
31
  onScrollRelease(callback: () => void): UIScrollable,
31
32
  /** Pulled past the content edge by `delta` px. */
32
33
  onOverscroll(callback: (delta: number) => void): UIScrollable
33
- /** Pull-to-refresh: the spinner stays until the returned promise settles. Attach BEFORE the
34
- * node mounts (hosts read it at creation). Vertical only; native hosts — no-op on web. */
34
+ /** Pull-to-refresh: the spinner stays until the returned promise settles. Vertical only. */
35
35
  onRefresh(callback: () => void | Promise<void>): UIScrollable
36
36
  }
37
37
 
38
38
  class ScrollableElement extends ContainerElement<"scrollable"> {
39
39
 
40
- readonly osl: any[] = []
41
- readonly sl: any[] = []
42
- readonly srl: any[] = []
43
-
44
- get scrollListeners() {
45
- return this.sl
46
- }
40
+ private readonly osl: any[] = []
41
+ private readonly sl: any[] = []
42
+ private readonly srl: any[] = []
43
+ private _refreshCallback?: () => void | Promise<void>
47
44
 
48
45
  onScroll (callback: any): this {
49
46
  this.sl.push(callback)
47
+ this._addFlags(TREE_FLAG_SCROLL)
50
48
  return this
51
49
  }
52
50
 
53
51
  onScrollRelease (callback: any): this {
54
52
  this.srl.push(callback)
53
+ this._addFlags(TREE_FLAG_SCROLL_RELEASE)
55
54
  return this
56
55
  }
57
56
 
58
57
  onOverscroll (callback: any): this {
59
58
  this.osl.push(callback)
59
+ this._addFlags(TREE_FLAG_OVERSCROLL)
60
60
  return this
61
61
  }
62
62
 
63
- // Read by the host at mount (same contract the scrollable screen had before 2026-07-22).
64
- declare _refreshCallback: any
65
63
  onRefresh (callback: any): this {
66
64
  this._refreshCallback = callback
65
+ this._addFlags(TREE_FLAG_REFRESH)
67
66
  return this
68
67
  }
68
+
69
+ /** @internal */
70
+ _emitScroll(position: number): void { for (const cb of this.sl.slice()) cb(position) }
71
+ /** @internal */
72
+ _emitScrollRelease(): void { for (const cb of this.srl.slice()) cb() }
73
+ /** @internal */
74
+ _emitOverscroll(delta: number): void { for (const cb of this.osl.slice()) cb(delta) }
75
+ /** @internal the host awaits the returned promise (its spinner) */
76
+ _emitRefresh(): unknown { return this._refreshCallback?.() }
69
77
  }
70
78
 
71
79
  // flexShrink: 1 default (SDK-owned, like UIRow's flexDirection): without it a scrollable in a
@@ -80,8 +88,6 @@ export const __UIScrollable = (children: UINodeChild[] | ChildrenFn): UIScrollab
80
88
  * `flexGrow: 1`. */
81
89
  export function UIScrollable(...children: UIChildArg[]): UIScrollable
82
90
  export function UIScrollable(children: UINodeChild[] | ChildrenFn): UIScrollable
83
- /** @deprecated Style as the first argument is legacy — use chained `.style({...})`. */
84
- export function UIScrollable(style: UIScrollableStyle, children?: UINodeChild[] | ChildrenFn): UIScrollable
85
91
  export function UIScrollable(...args: any[]): UIScrollable {
86
92
  return buildUI(args, SCROLLABLE_DEFAULTS, makeScrollable)
87
93
  }
@@ -11,4 +11,4 @@ export function UISpacer(): UISpacer
11
11
  export function UISpacer(style: UISpacerStyle): UISpacer
12
12
  export function UISpacer(style?: UISpacerStyle): UISpacer {
13
13
  return new Element("spacer", { flexGrow: 1, ...style })
14
- }
14
+ }
package/src/ui/UITabs.ts CHANGED
@@ -73,6 +73,7 @@ export class TabsElement extends ScreenElement {
73
73
 
74
74
  constructor(defs: Record<string, UITabDef>) {
75
75
  super("screen", { bgColor: SCREEN_BG }, [])
76
+ this.style({ bgColor: SCREEN_BG })
76
77
  this.ids = Object.keys(defs)
77
78
  if (this.ids.length === 0) console.warn("UITabs: no tabs declared")
78
79
 
@@ -94,16 +95,17 @@ export class TabsElement extends ScreenElement {
94
95
  this._badges.push(badge)
95
96
  this._badgeTexts.push(badgeText)
96
97
  const button = UIButton(anchor, icon ? label : null)
97
- .style({ name: `tab-${id}`, flexDirection: "column", gap: 3, flex: 1, pt: 8, pb: 6 })
98
+ .style({ flexDirection: "column", gap: 3, flex: 1, pt: 8, pb: 6 })
99
+ .named(`tab-${id}`)
98
100
  .onClick(() => this.select(id))
99
101
  this._buttons.push(button)
100
102
  return button
101
103
  })
102
104
 
103
105
  this._bar = UIRow(buttons).style({
104
- name: "tabbar", bgColor: BAR_BG, pb: "comfort-bottom",
106
+ bgColor: BAR_BG, pb: "comfort-bottom",
105
107
  borderTopWidth: 1, borderTopColor: BAR_BORDER,
106
- })
108
+ }).named("tabbar")
107
109
 
108
110
  this._pagerEl.onSelect((i) => {
109
111
  this._applyActive(i)
@@ -220,11 +222,11 @@ export function defineTabs<T extends Record<string, TabDef>>(tabs: T): TabsHandl
220
222
  return UIButton(
221
223
  def.icon ? UIImage(def.icon).style({ width: 22, height: 22, tintColor: color }) : null,
222
224
  UIText(def.label).style({ fontSize: 10, color }),
223
- ).style({ name: `tab-${id}`, flexDirection: "column", gap: 3, flex: 1, pt: 8, pb: 6 })
225
+ ).style({ flexDirection: "column", gap: 3, flex: 1, pt: 8, pb: 6 }).named(`tab-${id}`)
224
226
  })).style({
225
- name: "tabbar", bgColor: BAR_BG, pb: "comfort-bottom",
227
+ bgColor: BAR_BG, pb: "comfort-bottom",
226
228
  borderTopWidth: 1, borderTopColor: BAR_BORDER,
227
- })
229
+ }).named("tabbar")
228
230
 
229
231
  const screen = (active: keyof T & string, ...children: UIChildArg[]) =>
230
232
  UIScreen(