lecodes-sdk 0.19.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 (367) hide show
  1. package/README.md +114 -0
  2. package/dist/global.d.ts +503 -0
  3. package/dist/host.d.ts +61 -0
  4. package/dist/inject.js +4730 -0
  5. package/dist/types/animate/animate.d.ts +20 -0
  6. package/dist/types/animate/bezier.d.ts +2 -0
  7. package/dist/types/animate/easings.d.ts +21 -0
  8. package/dist/types/canvas/Canvas.d.ts +168 -0
  9. package/dist/types/core/Aspect.d.ts +171 -0
  10. package/dist/types/core/InspectorUI.d.ts +88 -0
  11. package/dist/types/core/StateMachine.d.ts +81 -0
  12. package/dist/types/core/color.d.ts +13 -0
  13. package/dist/types/core/compWrite.d.ts +11 -0
  14. package/dist/types/core/events.d.ts +11 -0
  15. package/dist/types/core/fields.d.ts +58 -0
  16. package/dist/types/core/registry.d.ts +7 -0
  17. package/dist/types/core/signals.d.ts +29 -0
  18. package/dist/types/core/time.d.ts +31 -0
  19. package/dist/types/g2/Camera2D.d.ts +19 -0
  20. package/dist/types/g2/CharacterController2D.d.ts +134 -0
  21. package/dist/types/g2/Node2D.d.ts +91 -0
  22. package/dist/types/g2/OneWay2D.d.ts +24 -0
  23. package/dist/types/g2/Physics2D.d.ts +147 -0
  24. package/dist/types/g2/Scene2D.d.ts +72 -0
  25. package/dist/types/g2/Shape2D.d.ts +58 -0
  26. package/dist/types/g2/Sprite.d.ts +51 -0
  27. package/dist/types/g2/SpriteAnimation.d.ts +50 -0
  28. package/dist/types/g2/SpriteSheet.d.ts +69 -0
  29. package/dist/types/g2/Texture2D.d.ts +16 -0
  30. package/dist/types/g2/Tilemap.d.ts +20 -0
  31. package/dist/types/g2/Tileset.d.ts +32 -0
  32. package/dist/types/g2/Trigger2D.d.ts +20 -0
  33. package/dist/types/g2/autotile.d.ts +133 -0
  34. package/dist/types/g2/cells.d.ts +14 -0
  35. package/dist/types/g2/defineScene2d.d.ts +141 -0
  36. package/dist/types/g2/groups2d.d.ts +16 -0
  37. package/dist/types/g2/loop.d.ts +4 -0
  38. package/dist/types/g2/scenarios2d.d.ts +30 -0
  39. package/dist/types/g2/touch.d.ts +2 -0
  40. package/dist/types/gl/Camera.d.ts +79 -0
  41. package/dist/types/gl/CameraPlace.d.ts +26 -0
  42. package/dist/types/gl/CharacterController.d.ts +98 -0
  43. package/dist/types/gl/Gearbox.d.ts +86 -0
  44. package/dist/types/gl/Geometry.d.ts +39 -0
  45. package/dist/types/gl/IK.d.ts +53 -0
  46. package/dist/types/gl/InstancedMesh.d.ts +46 -0
  47. package/dist/types/gl/Light.d.ts +73 -0
  48. package/dist/types/gl/Lightmap.d.ts +85 -0
  49. package/dist/types/gl/Locomotion.d.ts +279 -0
  50. package/dist/types/gl/Material.d.ts +100 -0
  51. package/dist/types/gl/Mesh.d.ts +33 -0
  52. package/dist/types/gl/Model.d.ts +53 -0
  53. package/dist/types/gl/NavAgent.d.ts +99 -0
  54. package/dist/types/gl/NavMesh.d.ts +169 -0
  55. package/dist/types/gl/Node.d.ts +99 -0
  56. package/dist/types/gl/Noise.d.ts +20 -0
  57. package/dist/types/gl/Particles.d.ts +277 -0
  58. package/dist/types/gl/Physics.d.ts +91 -0
  59. package/dist/types/gl/Plane.d.ts +19 -0
  60. package/dist/types/gl/Ragdoll.d.ts +86 -0
  61. package/dist/types/gl/Ray.d.ts +8 -0
  62. package/dist/types/gl/Scene.d.ts +221 -0
  63. package/dist/types/gl/Shape.d.ts +122 -0
  64. package/dist/types/gl/Terrain.d.ts +280 -0
  65. package/dist/types/gl/Texture.d.ts +27 -0
  66. package/dist/types/gl/Trigger.d.ts +10 -0
  67. package/dist/types/gl/Vehicle.d.ts +191 -0
  68. package/dist/types/gl/Wheel.d.ts +95 -0
  69. package/dist/types/gl/animation/AnimationClip.d.ts +60 -0
  70. package/dist/types/gl/animation/Animator.d.ts +219 -0
  71. package/dist/types/gl/animation/Layer.d.ts +31 -0
  72. package/dist/types/gl/animation/Loop.d.ts +17 -0
  73. package/dist/types/gl/animation/Playback.d.ts +36 -0
  74. package/dist/types/gl/animation/core.d.ts +367 -0
  75. package/dist/types/gl/controls.d.ts +19 -0
  76. package/dist/types/gl/physicsEvents.d.ts +1 -0
  77. package/dist/types/gl/scenarios.d.ts +98 -0
  78. package/dist/types/gl/state.d.ts +5 -0
  79. package/dist/types/gl/terrainMesh.d.ts +46 -0
  80. package/dist/types/gl/touch.d.ts +2 -0
  81. package/dist/types/inject.d.ts +123 -0
  82. package/dist/types/math/Mathf.d.ts +39 -0
  83. package/dist/types/math/mat4.d.ts +66 -0
  84. package/dist/types/math/quat.d.ts +53 -0
  85. package/dist/types/math/vec.d.ts +129 -0
  86. package/dist/types/net/codec.d.ts +25 -0
  87. package/dist/types/net/core.d.ts +106 -0
  88. package/dist/types/net/index.d.ts +80 -0
  89. package/dist/types/net/replication.d.ts +101 -0
  90. package/dist/types/plugins/camera.d.ts +25 -0
  91. package/dist/types/plugins/geolocation.d.ts +39 -0
  92. package/dist/types/plugins/oauth.d.ts +25 -0
  93. package/dist/types/plugins/permission.d.ts +1 -0
  94. package/dist/types/plugins/push.d.ts +46 -0
  95. package/dist/types/plugins/qr.d.ts +21 -0
  96. package/dist/types/plugins/service.d.ts +33 -0
  97. package/dist/types/runtime/app.d.ts +67 -0
  98. package/dist/types/runtime/appEvents.d.ts +7 -0
  99. package/dist/types/runtime/channel.d.ts +1 -0
  100. package/dist/types/runtime/clipboard.d.ts +7 -0
  101. package/dist/types/runtime/datetime.d.ts +80 -0
  102. package/dist/types/runtime/device.d.ts +126 -0
  103. package/dist/types/runtime/fetch.d.ts +31 -0
  104. package/dist/types/runtime/files.d.ts +33 -0
  105. package/dist/types/runtime/input.d.ts +121 -0
  106. package/dist/types/runtime/media.d.ts +46 -0
  107. package/dist/types/runtime/misc.d.ts +11 -0
  108. package/dist/types/runtime/net.d.ts +15 -0
  109. package/dist/types/runtime/rpc.d.ts +44 -0
  110. package/dist/types/runtime/service.d.ts +1 -0
  111. package/dist/types/runtime/share.d.ts +2 -0
  112. package/dist/types/runtime/storage.d.ts +5 -0
  113. package/dist/types/runtime/touch.d.ts +56 -0
  114. package/dist/types/scene/defineScene.d.ts +296 -0
  115. package/dist/types/scene/editorPlugins.d.ts +82 -0
  116. package/dist/types/scene/gizmos.d.ts +56 -0
  117. package/dist/types/scene/grammar.d.ts +56 -0
  118. package/dist/types/scene/level.d.ts +98 -0
  119. package/dist/types/scene/material.d.ts +76 -0
  120. package/dist/types/server/auth/appConfig.d.ts +16 -0
  121. package/dist/types/server/auth/global.d.ts +56 -0
  122. package/dist/types/server/auth/models.d.ts +164 -0
  123. package/dist/types/server/auth/types.d.ts +56 -0
  124. package/dist/types/server/channel.d.ts +28 -0
  125. package/dist/types/server/context.d.ts +27 -0
  126. package/dist/types/server/db/defineDb.d.ts +36 -0
  127. package/dist/types/server/db/fields.d.ts +146 -0
  128. package/dist/types/server/db/index.d.ts +6 -0
  129. package/dist/types/server/db/marci/query.d.ts +326 -0
  130. package/dist/types/server/db/types.d.ts +163 -0
  131. package/dist/types/server/errors.d.ts +8 -0
  132. package/dist/types/server/inject.d.ts +5 -0
  133. package/dist/types/ui/NativeView.d.ts +38 -0
  134. package/dist/types/ui/UI.d.ts +27 -0
  135. package/dist/types/ui/UIBottomSheet.d.ts +71 -0
  136. package/dist/types/ui/UIButton.d.ts +30 -0
  137. package/dist/types/ui/UIContainer.d.ts +33 -0
  138. package/dist/types/ui/UIImage.d.ts +35 -0
  139. package/dist/types/ui/UIInput.d.ts +110 -0
  140. package/dist/types/ui/UIModal.d.ts +70 -0
  141. package/dist/types/ui/UINode.d.ts +363 -0
  142. package/dist/types/ui/UIPager.d.ts +123 -0
  143. package/dist/types/ui/UIPopover.d.ts +39 -0
  144. package/dist/types/ui/UIScreen.d.ts +57 -0
  145. package/dist/types/ui/UIScrollable.d.ts +40 -0
  146. package/dist/types/ui/UISpacer.d.ts +9 -0
  147. package/dist/types/ui/UITabs.d.ts +96 -0
  148. package/dist/types/ui/UIText.d.ts +19 -0
  149. package/dist/types/ui/UIVideo.d.ts +28 -0
  150. package/dist/types/ui/UIVirtualizedList.d.ts +97 -0
  151. package/dist/types/ui/UIWidget.d.ts +65 -0
  152. package/dist/types/ui/fonts.d.ts +9 -0
  153. package/dist/types/ui/presentable.d.ts +68 -0
  154. package/dist/types/ui/router.d.ts +40 -0
  155. package/dist/types/ui/theme.d.ts +26 -0
  156. package/dist/types.json +1 -0
  157. package/package.json +46 -0
  158. package/prompts/2d.md +395 -0
  159. package/prompts/3d-scene-files.md +113 -0
  160. package/prompts/3d-scene.md +119 -0
  161. package/prompts/3d.md +303 -0
  162. package/prompts/README.md +142 -0
  163. package/prompts/ar.md +109 -0
  164. package/prompts/canvas.md +27 -0
  165. package/prompts/compose.ts +46 -0
  166. package/prompts/concept.md +49 -0
  167. package/prompts/core-design.md +75 -0
  168. package/prompts/core.md +264 -0
  169. package/prompts/design.md +218 -0
  170. package/prompts/dist/2d-game.md +1530 -0
  171. package/prompts/dist/3d-app.md +1672 -0
  172. package/prompts/dist/ar-app.md +1548 -0
  173. package/prompts/dist/concept.md +49 -0
  174. package/prompts/dist/design.md +498 -0
  175. package/prompts/dist/ui-app.md +1134 -0
  176. package/prompts/index.md +37 -0
  177. package/prompts/intake-prompt.md +33 -0
  178. package/prompts/namer-prompt.md +12 -0
  179. package/prompts/router-prompt.md +50 -0
  180. package/prompts/select.ts +111 -0
  181. package/prompts/ui-design.md +203 -0
  182. package/prompts/ui.md +803 -0
  183. package/src/animate/animate.ts +238 -0
  184. package/src/animate/bezier.ts +138 -0
  185. package/src/animate/easings.ts +126 -0
  186. package/src/bridges.d.ts +1345 -0
  187. package/src/canvas/Canvas.ts +305 -0
  188. package/src/compile/__tests__/assetIconMacro.test.ts +219 -0
  189. package/src/compile/__tests__/assetMacro.test.ts +74 -0
  190. package/src/compile/__tests__/assetName.test.ts +55 -0
  191. package/src/compile/__tests__/compile.test.ts +287 -0
  192. package/src/compile/__tests__/detectEntry.test.ts +132 -0
  193. package/src/compile/__tests__/fontMacro.test.ts +199 -0
  194. package/src/compile/aspectMacro.ts +86 -0
  195. package/src/compile/assetIconMacro.ts +384 -0
  196. package/src/compile/assetMacro.ts +146 -0
  197. package/src/compile/assetName.ts +50 -0
  198. package/src/compile/bundler.ts +287 -0
  199. package/src/compile/compileProject.ts +127 -0
  200. package/src/compile/detectEntry.ts +128 -0
  201. package/src/compile/fontMacro.ts +459 -0
  202. package/src/compile/fontRegistry.ts +78 -0
  203. package/src/compile/header.ts +67 -0
  204. package/src/compile/index.ts +100 -0
  205. package/src/compile/libraryImports.ts +52 -0
  206. package/src/compile/liteMaterial.ts +247 -0
  207. package/src/compile/sceneEditor.ts +88 -0
  208. package/src/compile/serverSplit.ts +233 -0
  209. package/src/compile/serverTypes.ts +227 -0
  210. package/src/compile/sfnt.ts +98 -0
  211. package/src/compile/shaderSchema.ts +202 -0
  212. package/src/compile/shaderTargets.ts +77 -0
  213. package/src/compile/sourcemap.ts +25 -0
  214. package/src/core/Aspect.ts +568 -0
  215. package/src/core/InspectorUI.ts +212 -0
  216. package/src/core/StateMachine.ts +308 -0
  217. package/src/core/__tests__/stateMachine.test.ts +132 -0
  218. package/src/core/color.ts +66 -0
  219. package/src/core/compWrite.ts +42 -0
  220. package/src/core/events.ts +38 -0
  221. package/src/core/fields.ts +120 -0
  222. package/src/core/registry.ts +23 -0
  223. package/src/core/signals.ts +277 -0
  224. package/src/core/time.ts +84 -0
  225. package/src/g2/Camera2D.ts +40 -0
  226. package/src/g2/CharacterController2D.ts +276 -0
  227. package/src/g2/Node2D.ts +267 -0
  228. package/src/g2/OneWay2D.ts +66 -0
  229. package/src/g2/Physics2D.ts +346 -0
  230. package/src/g2/Scene2D.ts +209 -0
  231. package/src/g2/Shape2D.ts +259 -0
  232. package/src/g2/Sprite.ts +89 -0
  233. package/src/g2/SpriteAnimation.ts +171 -0
  234. package/src/g2/SpriteSheet.ts +166 -0
  235. package/src/g2/Texture2D.ts +47 -0
  236. package/src/g2/Tilemap.ts +41 -0
  237. package/src/g2/Tileset.ts +71 -0
  238. package/src/g2/Trigger2D.ts +77 -0
  239. package/src/g2/autotile.ts +433 -0
  240. package/src/g2/cells.ts +91 -0
  241. package/src/g2/defineScene2d.ts +381 -0
  242. package/src/g2/groups2d.ts +106 -0
  243. package/src/g2/loop.ts +50 -0
  244. package/src/g2/scenarios2d.ts +69 -0
  245. package/src/g2/touch.ts +83 -0
  246. package/src/gl/Camera.ts +160 -0
  247. package/src/gl/CameraPlace.ts +52 -0
  248. package/src/gl/CharacterController.ts +238 -0
  249. package/src/gl/Gearbox.ts +212 -0
  250. package/src/gl/Geometry.ts +279 -0
  251. package/src/gl/IK.ts +193 -0
  252. package/src/gl/InstancedMesh.ts +132 -0
  253. package/src/gl/Light.ts +135 -0
  254. package/src/gl/Lightmap.ts +402 -0
  255. package/src/gl/Locomotion.ts +498 -0
  256. package/src/gl/Material.ts +279 -0
  257. package/src/gl/Mesh.ts +83 -0
  258. package/src/gl/Model.ts +124 -0
  259. package/src/gl/NavAgent.ts +337 -0
  260. package/src/gl/NavMesh.ts +397 -0
  261. package/src/gl/Node.ts +350 -0
  262. package/src/gl/Noise.ts +30 -0
  263. package/src/gl/Particles.ts +676 -0
  264. package/src/gl/Physics.ts +222 -0
  265. package/src/gl/Plane.ts +53 -0
  266. package/src/gl/Ragdoll.ts +270 -0
  267. package/src/gl/Ray.ts +16 -0
  268. package/src/gl/Scene.ts +505 -0
  269. package/src/gl/Shape.ts +409 -0
  270. package/src/gl/Terrain.ts +1092 -0
  271. package/src/gl/Texture.ts +63 -0
  272. package/src/gl/Trigger.ts +45 -0
  273. package/src/gl/Vehicle.ts +473 -0
  274. package/src/gl/Wheel.ts +240 -0
  275. package/src/gl/animation/AnimationClip.ts +204 -0
  276. package/src/gl/animation/Animator.ts +329 -0
  277. package/src/gl/animation/Layer.ts +39 -0
  278. package/src/gl/animation/Loop.ts +31 -0
  279. package/src/gl/animation/Playback.ts +52 -0
  280. package/src/gl/animation/core.ts +670 -0
  281. package/src/gl/controls.ts +95 -0
  282. package/src/gl/physicsEvents.ts +20 -0
  283. package/src/gl/scenarios.ts +291 -0
  284. package/src/gl/state.ts +6 -0
  285. package/src/gl/terrainMesh.ts +219 -0
  286. package/src/gl/touch.ts +68 -0
  287. package/src/host.d.ts +61 -0
  288. package/src/inject.ts +203 -0
  289. package/src/math/Mathf.ts +118 -0
  290. package/src/math/mat4.ts +278 -0
  291. package/src/math/quat.ts +232 -0
  292. package/src/math/vec.ts +255 -0
  293. package/src/net/codec.ts +119 -0
  294. package/src/net/core.ts +384 -0
  295. package/src/net/index.ts +181 -0
  296. package/src/net/replication.ts +622 -0
  297. package/src/plugins/camera.ts +81 -0
  298. package/src/plugins/geolocation.ts +123 -0
  299. package/src/plugins/oauth.ts +61 -0
  300. package/src/plugins/permission.ts +7 -0
  301. package/src/plugins/push.ts +132 -0
  302. package/src/plugins/qr.ts +73 -0
  303. package/src/plugins/service.ts +47 -0
  304. package/src/runtime/app.ts +101 -0
  305. package/src/runtime/appEvents.ts +54 -0
  306. package/src/runtime/channel.ts +61 -0
  307. package/src/runtime/clipboard.ts +20 -0
  308. package/src/runtime/datetime.ts +329 -0
  309. package/src/runtime/device.ts +293 -0
  310. package/src/runtime/fetch.ts +77 -0
  311. package/src/runtime/files.ts +108 -0
  312. package/src/runtime/input.ts +175 -0
  313. package/src/runtime/media.ts +111 -0
  314. package/src/runtime/misc.ts +16 -0
  315. package/src/runtime/net.ts +36 -0
  316. package/src/runtime/rpc.ts +218 -0
  317. package/src/runtime/service.ts +83 -0
  318. package/src/runtime/share.ts +9 -0
  319. package/src/runtime/storage.ts +13 -0
  320. package/src/runtime/touch.ts +76 -0
  321. package/src/scene/defineScene.ts +1396 -0
  322. package/src/scene/editorPlugins.ts +110 -0
  323. package/src/scene/gizmos.ts +148 -0
  324. package/src/scene/grammar.ts +120 -0
  325. package/src/scene/level.ts +296 -0
  326. package/src/scene/material.ts +188 -0
  327. package/src/server/auth/appConfig.ts +12 -0
  328. package/src/server/auth/global.ts +80 -0
  329. package/src/server/auth/host.ts +318 -0
  330. package/src/server/auth/models.ts +83 -0
  331. package/src/server/auth/types.ts +50 -0
  332. package/src/server/channel.ts +56 -0
  333. package/src/server/context.ts +36 -0
  334. package/src/server/db/defineDb.ts +237 -0
  335. package/src/server/db/fields.ts +132 -0
  336. package/src/server/db/httpTransport.ts +93 -0
  337. package/src/server/db/index.ts +7 -0
  338. package/src/server/db/marci/query.ts +412 -0
  339. package/src/server/db/types.ts +202 -0
  340. package/src/server/errors.ts +12 -0
  341. package/src/server/host.ts +74 -0
  342. package/src/server/inject.ts +13 -0
  343. package/src/server/runtime.ts +133 -0
  344. package/src/server/validate.ts +87 -0
  345. package/src/ui/NativeView.ts +142 -0
  346. package/src/ui/UI.ts +39 -0
  347. package/src/ui/UIBottomSheet.ts +139 -0
  348. package/src/ui/UIButton.ts +101 -0
  349. package/src/ui/UIContainer.ts +60 -0
  350. package/src/ui/UIImage.ts +83 -0
  351. package/src/ui/UIInput.ts +185 -0
  352. package/src/ui/UIModal.ts +139 -0
  353. package/src/ui/UINode.ts +830 -0
  354. package/src/ui/UIPager.ts +362 -0
  355. package/src/ui/UIPopover.ts +100 -0
  356. package/src/ui/UIScreen.ts +123 -0
  357. package/src/ui/UIScrollable.ts +87 -0
  358. package/src/ui/UISpacer.ts +14 -0
  359. package/src/ui/UITabs.ts +236 -0
  360. package/src/ui/UIText.ts +51 -0
  361. package/src/ui/UIVideo.ts +88 -0
  362. package/src/ui/UIVirtualizedList.ts +241 -0
  363. package/src/ui/UIWidget.ts +127 -0
  364. package/src/ui/fonts.ts +13 -0
  365. package/src/ui/presentable.ts +117 -0
  366. package/src/ui/router.ts +132 -0
  367. package/src/ui/theme.ts +84 -0
@@ -0,0 +1,14 @@
1
+ import { Element, type ElementStyle, type UIElementBase } from "./UINode"
2
+
3
+ export type UISpacerStyle = ElementStyle
4
+
5
+ /** A flexible gap — `flexGrow: 1` by default, pushes its siblings apart. */
6
+ export interface UISpacer extends UIElementBase<UISpacerStyle> {
7
+ readonly type: "spacer",
8
+ }
9
+ /** Create a flexible gap (`flexGrow: 1`) — e.g. between a header's title and its actions. */
10
+ export function UISpacer(): UISpacer
11
+ export function UISpacer(style: UISpacerStyle): UISpacer
12
+ export function UISpacer(style?: UISpacerStyle): UISpacer {
13
+ return new Element("spacer", { flexGrow: 1, ...style })
14
+ }
@@ -0,0 +1,236 @@
1
+ import { PagerElement, type UIPager } from "./UIPager"
2
+ import { ScreenElement, UIScreen } from "./UIScreen"
3
+ import { UIButton } from "./UIButton"
4
+ import { UIRow, UIColumn, type UIRow as UIRowType } from "./UIContainer"
5
+ import { UIText } from "./UIText"
6
+ import { UIImage, type ImageSource } from "./UIImage"
7
+ import type { UIChildArg } from "./UINode"
8
+
9
+ // UITabs — the standard bottom-tab shell, prewired: a UIScreen composing [ UIPager (flexGrow: 1),
10
+ // tab bar ]. Pure SDK composition over the pager — zero new ABI, works wherever UIPager does.
11
+ // `defineTabs` (below) is its design-stage twin (lecodes-design/tabbar-surface.md): the same keyed
12
+ // config without screens, mounted per-file with a literal tab id. They share the constants and bar
13
+ // geometry in this file, so a designed mockup and the built app render identically.
14
+
15
+ /** Active/inactive/bar colors ride the theme — one `theme({ primaryColor, mutedColor, tabbarBg })`
16
+ * call restyles the bar app-wide; the fallbacks keep an unthemed app looking right. */
17
+ const ACTIVE = "var(--primaryColor, #5b8cff)"
18
+ const INACTIVE = "var(--mutedColor, #8a919e)"
19
+ const BAR_BG = "var(--tabbarBg, #15171c)"
20
+ const BAR_BORDER = "var(--tabbarBorder, #23262e)"
21
+ const SCREEN_BG = "var(--screenBg, #101114)"
22
+ const BADGE_BG = "var(--badgeColor, #ff453a)"
23
+
24
+ export type UITabDef = {
25
+ label: string,
26
+ /** Tab icon — typically `assetIcon("lucide:…")`. Tinted active/inactive by the bar. */
27
+ icon?: ImageSource,
28
+ /** The tab's root screen. Tabs are fixed at construction (like UIPager's). */
29
+ screen: UIScreen,
30
+ }
31
+
32
+ /**
33
+ * A bottom-tab app shell: swipeable tabs (a `UIPager`) plus a themed tab bar, as one
34
+ * `UIScreen` — present it directly (`Router.init(tabs)`). Keys are tab ids, in tab order:
35
+ *
36
+ * ```ts
37
+ * const tabs = UITabs({
38
+ * home: { label: "Home", icon: assetIcon("lucide:house"), screen: homeScreen },
39
+ * profile: { label: "Profile", icon: assetIcon("lucide:user"), screen: profileScreen },
40
+ * })
41
+ * Router.init(tabs)
42
+ * ```
43
+ *
44
+ * In-tab navigation stays the pager's: `UIPager.push(detail)` from any screen (bar stays);
45
+ * `Router.push` opens above the shell (bar covered). Restyle via `theme()` (primaryColor,
46
+ * mutedColor, tabbarBg, …) or the `bar` handle; for a fully custom bar build on `UIPager`.
47
+ */
48
+ export interface UITabs extends UIScreen {
49
+ /** The active tab id. */
50
+ readonly tab: string,
51
+ /** Switch tabs (instant, the tab-bar convention; `animated: true` slides). */
52
+ select(id: string, animated?: boolean): this,
53
+ /** Fires when the active tab changes and has settled — a bar tap, swipe, or `select()`. */
54
+ onSelect(callback: (id: string, index: number) => void): this,
55
+ /** Show a badge on a tab: `true` = dot, a number/string = count pill, `false`/`null`/`0` clears. */
56
+ badge(id: string, value: number | string | boolean | null): this,
57
+ /** The pager under the bar — in-tab stacks (`push`/`pop`/`depth`). */
58
+ readonly pager: UIPager,
59
+ /** The bar row (named "tabbar") — for restyling beyond the theme variables. */
60
+ readonly bar: UIRowType,
61
+ }
62
+
63
+ export class TabsElement extends ScreenElement {
64
+ private readonly ids: string[]
65
+ private readonly _pagerEl: PagerElement
66
+ private readonly _bar: any
67
+ private readonly _icons: (any | null)[] = []
68
+ private readonly _labels: any[] = []
69
+ private readonly _badges: any[] = []
70
+ private readonly _badgeTexts: any[] = []
71
+ private readonly _buttons: any[] = []
72
+ private readonly _selectListeners: ((id: string, index: number) => void)[] = []
73
+
74
+ constructor(defs: Record<string, UITabDef>) {
75
+ super("screen", { bgColor: SCREEN_BG }, [])
76
+ this.ids = Object.keys(defs)
77
+ if (this.ids.length === 0) console.warn("UITabs: no tabs declared")
78
+
79
+ this._pagerEl = new PagerElement({ flexGrow: 1 }, this.ids.map((id) => defs[id].screen))
80
+
81
+ const buttons = this.ids.map((id, i) => {
82
+ const def = defs[id]
83
+ const icon = def.icon ? UIImage(def.icon).style({ width: 22, height: 22, tintColor: INACTIVE }) : null
84
+ const label = UIText(def.label).style({ fontSize: 10, color: INACTIVE })
85
+ const badgeText = UIText("").style({ fontSize: 9, fontWeight: 700, color: "white" })
86
+ const badge = UIRow(badgeText).style({
87
+ position: "absolute", top: -3, right: -6, display: "none",
88
+ bgColor: BADGE_BG, borderRadius: 99, justifyContent: "center", alignItems: "center",
89
+ })
90
+ // The badge anchors to the icon (or the label when there is none) — its wrap must not clip.
91
+ const anchor = UIColumn(icon ?? label, badge).style({ overflow: "visible", alignItems: "center" })
92
+ this._icons.push(icon)
93
+ this._labels.push(label)
94
+ this._badges.push(badge)
95
+ this._badgeTexts.push(badgeText)
96
+ const button = UIButton(anchor, icon ? label : null)
97
+ .style({ name: `tab-${id}`, flexDirection: "column", gap: 3, flex: 1, pt: 8, pb: 6 })
98
+ .onClick(() => this.select(id))
99
+ this._buttons.push(button)
100
+ return button
101
+ })
102
+
103
+ this._bar = UIRow(buttons).style({
104
+ name: "tabbar", bgColor: BAR_BG, pb: "comfort-bottom",
105
+ borderTopWidth: 1, borderTopColor: BAR_BORDER,
106
+ })
107
+
108
+ this._pagerEl.onSelect((i) => {
109
+ this._applyActive(i)
110
+ for (const cb of this._selectListeners) cb(this.ids[i], i)
111
+ })
112
+ this._applyActive(0)
113
+ this.append(this._pagerEl as any, this._bar)
114
+ }
115
+
116
+ private _applyActive(index: number): void {
117
+ for (let i = 0; i < this.ids.length; i++) {
118
+ const on = i === index
119
+ const color = on ? ACTIVE : INACTIVE
120
+ // Imperative writes carry the visual on every host; the class is the styling hook — custom
121
+ // content inside a tab button can declare `$active` blocks (class state cascades down).
122
+ this._buttons[i].class.active = on
123
+ if (this._icons[i]) this._icons[i].style.tintColor = color
124
+ this._labels[i].style.color = color
125
+ }
126
+ }
127
+
128
+ get tab(): string {
129
+ return this.ids[this._pagerEl.index]
130
+ }
131
+ get pager(): any {
132
+ return this._pagerEl
133
+ }
134
+ get bar(): any {
135
+ return this._bar
136
+ }
137
+
138
+ select(id: string, animated = false): this {
139
+ const index = this.ids.indexOf(id)
140
+ if (index < 0) {
141
+ console.warn(`UITabs.select: unknown tab "${id}"`)
142
+ return this
143
+ }
144
+ this._pagerEl.select(index, animated)
145
+ return this
146
+ }
147
+
148
+ onSelect(callback: (id: string, index: number) => void): this {
149
+ this._selectListeners.push(callback)
150
+ return this
151
+ }
152
+
153
+ badge(id: string, value: number | string | boolean | null | undefined): this {
154
+ const i = this.ids.indexOf(id)
155
+ if (i < 0) {
156
+ console.warn(`UITabs.badge: unknown tab "${id}"`)
157
+ return this
158
+ }
159
+ if (value === null || value === undefined || value === false || value === 0 || value === "") {
160
+ this._badges[i].style.display = "none"
161
+ } else if (value === true) {
162
+ this._badgeTexts[i].text = ""
163
+ this._badges[i].style({ display: "flex", width: 9, height: 9, minWidth: 0, px: 0 })
164
+ } else {
165
+ this._badgeTexts[i].text = String(value)
166
+ this._badges[i].style({ display: "flex", width: "auto", height: 14, minWidth: 14, px: 4 })
167
+ }
168
+ return this
169
+ }
170
+ }
171
+
172
+ /** Create the standard bottom-tab shell — see {@link UITabs}. Keys are tab ids in tab order;
173
+ * each tab needs a `label` and its root `screen` (plus an optional `icon`). */
174
+ export function UITabs(tabs: Record<string, UITabDef>): UITabs {
175
+ return new TabsElement(tabs) as unknown as UITabs
176
+ }
177
+
178
+ // ---- defineTabs — the design-stage twin ---------------------------------------------------------
179
+
180
+ /** A `defineTabs` tab: `label` + optional `icon` (an `assetIcon("lucide:…")` call — never a
181
+ * hand-written source object). `UITabDef` is this plus the tab's root `screen`. */
182
+ export type TabDef = Omit<UITabDef, "screen">
183
+
184
+ /** What `defineTabs` returns: the bar (mount it yourself when the screen owns its whole layout)
185
+ * and the standard screen shell around it. `active` must be a declared tab id, as a literal. */
186
+ export type TabsHandle<T extends Record<string, TabDef>> = {
187
+ /** Tab ids in declaration order. */
188
+ ids: (keyof T & string)[]
189
+ /** The bar alone — one button per tab, the `active` one highlighted. */
190
+ bar(active: keyof T & string): UIRowType
191
+ /** A screen sitting ON the bar: `[ padded content column, bar ]`. */
192
+ screen(active: keyof T & string, ...children: UIChildArg[]): UIScreen
193
+ }
194
+
195
+ /**
196
+ * Declare a tab bar WITHOUT screens — the design-stage twin of {@link UITabs} (same keys, same
197
+ * `tab-<id>` button names, same theme variables), for mockups where every screen is its own file
198
+ * and mounts the bar with a literal tab id:
199
+ *
200
+ * ```ts
201
+ * export const mainTabs = defineTabs({
202
+ * home: { label: "Home", icon: assetIcon("lucide:house") },
203
+ * profile: { label: "Profile", icon: assetIcon("lucide:user") },
204
+ * })
205
+ * // in a screen file:
206
+ * export default () => mainTabs.screen("home", [ ...content ])
207
+ * ```
208
+ *
209
+ * Restyle through the theme — `theme({ primaryColor, mutedColor, tabbarBg, screenBg })` recolors
210
+ * every bar at once. The built app passes the same keys (plus each tab's root screen) to `UITabs`,
211
+ * which renders the identical bar interactively.
212
+ */
213
+ export function defineTabs<T extends Record<string, TabDef>>(tabs: T): TabsHandle<T> {
214
+ const ids = Object.keys(tabs) as (keyof T & string)[]
215
+
216
+ const bar = (active: keyof T & string) =>
217
+ UIRow(ids.map((id) => {
218
+ const def = tabs[id]
219
+ const color = id === active ? ACTIVE : INACTIVE
220
+ return UIButton(
221
+ def.icon ? UIImage(def.icon).style({ width: 22, height: 22, tintColor: color }) : null,
222
+ UIText(def.label).style({ fontSize: 10, color }),
223
+ ).style({ name: `tab-${id}`, flexDirection: "column", gap: 3, flex: 1, pt: 8, pb: 6 })
224
+ })).style({
225
+ name: "tabbar", bgColor: BAR_BG, pb: "comfort-bottom",
226
+ borderTopWidth: 1, borderTopColor: BAR_BORDER,
227
+ })
228
+
229
+ const screen = (active: keyof T & string, ...children: UIChildArg[]) =>
230
+ UIScreen(
231
+ UIColumn(...children).style({ flexGrow: 1, p: 16, pt: "safe-top" }),
232
+ bar(active),
233
+ ).style({ bgColor: SCREEN_BG })
234
+
235
+ return { ids, bar, screen }
236
+ }
@@ -0,0 +1,51 @@
1
+ import { createBinding } from "../core/signals"
2
+ import { Element, type BaseStyle, type DrawableStyle, type ElementStyle, type TextStyle, type UIElementBase } from "./UINode"
3
+
4
+ export type UITextStyle = ElementStyle & TextStyle & DrawableStyle
5
+
6
+ /** A text label. */
7
+ export interface UIText extends UIElementBase<UITextStyle, DrawableStyle & BaseStyle & TextStyle> {
8
+ readonly type: "text",
9
+ /** The displayed string — writable at any time. */
10
+ text: string,
11
+ }
12
+
13
+ export class TextElement extends Element<"text"> {
14
+ private _text: string
15
+ constructor(style: any, text: string | (() => string)) {
16
+ super("text", style)
17
+ if (typeof text === "function") {
18
+ // Reactive content: re-runs when a signal `text()` read changes; the setter routes to
19
+ // _creatorUI.updateNode when mounted and just keeps _text current when not.
20
+ this._text = ""
21
+ createBinding(this, "#text", () => {
22
+ const value = text()
23
+ if (value !== this._text) this.text = value
24
+ })
25
+ } else {
26
+ this._text = text
27
+ }
28
+ }
29
+ get text(): string {
30
+ return this._text
31
+ }
32
+ set text(value) {
33
+ this._text = value
34
+ if (this._id !== 0) {
35
+ _creatorUI.updateNode(this._id, "text", value)
36
+ }
37
+ }
38
+ }
39
+
40
+ /** Create a text label — a string, or `() => string` for reactive text that re-renders when a
41
+ * signal it read changes. */
42
+ export function UIText(text: string | (() => string)): UIText;
43
+ /** @deprecated Style as the first argument is legacy — use chained `.style({...})`. */
44
+ export function UIText(style: UITextStyle, text: string | (() => string)): UIText;
45
+ export function UIText(...args: [string | (() => string)] | [UITextStyle, string | (() => string)]): UIText {
46
+ if (args.length === 1) {
47
+ return new TextElement({}, args[0])
48
+ } else {
49
+ return new TextElement(args[0], args[1])
50
+ }
51
+ }
@@ -0,0 +1,88 @@
1
+ import type { VideoPlayer } from "../runtime/media"
2
+ import { _bumpNavEpoch, _navSupported, _setCurrent, Presentable, type PresentOptions } from "./presentable"
3
+ import { Element, type BaseStyle, type DrawableStyle, type ElementStyle, type UIElementBase } from "./UINode"
4
+
5
+ export type UIVideoStyle = ElementStyle & DrawableStyle & { objectFit?: "cover" | "contain" | "fill" }
6
+
7
+ /** A video surface showing a `VideoPlayer`'s output — embeddable in a screen AND
8
+ * fullscreen-presentable (`open()`), with seamless promotion between the two. */
9
+ export interface UIVideo extends UIElementBase<UIVideoStyle, DrawableStyle & BaseStyle> {
10
+ readonly type: "video",
11
+ /** The player driving this surface (playback control lives on it). */
12
+ player: VideoPlayer
13
+
14
+ /** Show fullscreen as the current destination. Promotion: if this video is mounted in a screen,
15
+ * the SAME native player moves fullscreen — playback uninterrupted; a pop moves it back. */
16
+ open(options?: PresentOptions): void
17
+ /** Dismiss if this is the visible destination. */
18
+ close(): void
19
+ /** Became the visible destination. */
20
+ onOpen(callback: () => void): this
21
+ /** Stopped being the visible destination. */
22
+ onClose(callback: () => void): this
23
+ /** Hardware/system back while current. */
24
+ onBackPressed(callback: () => void): this
25
+ }
26
+
27
+ class VideoElement extends Element<"video"> implements Presentable {
28
+ private _playerId: number
29
+ private _player: VideoPlayer
30
+ constructor(style: any, player: VideoPlayer) {
31
+ super("video", style)
32
+ this._player = player
33
+ this._playerId = (player as any)._id
34
+ }
35
+
36
+ get player() {
37
+ return this._player
38
+ }
39
+
40
+ // ---- Presentable (docs/navigation-presentable-plan.md) ----
41
+ readonly ol: (() => void)[] = []
42
+ readonly cl: (() => void)[] = []
43
+ _backButtonCallback?: () => void
44
+ private _vd?: object
45
+
46
+ /** @internal wire descriptor — type "videoView" (distinct from the "video" node type). */
47
+ _viewDesc(): object {
48
+ return this._vd ??= { type: "videoView", node: this, _p: this }
49
+ }
50
+
51
+ open(options?: PresentOptions): void {
52
+ if (!_navSupported()) {
53
+ throw new Error("Fullscreen video is not supported on this host (requires _creatorUI.openView)")
54
+ }
55
+ _bumpNavEpoch()
56
+ _creatorUI.openView!(this._viewDesc(), options?.transition ?? "none")
57
+ _setCurrent(this)
58
+ }
59
+ close(): void {
60
+ _bumpNavEpoch()
61
+ if (_navSupported()) _creatorUI.closeView!()
62
+ if (Presentable.current === this) _setCurrent(null)
63
+ }
64
+ onOpen(callback: () => void): this {
65
+ this.ol.push(callback)
66
+ return this
67
+ }
68
+ onClose(callback: () => void): this {
69
+ this.cl.push(callback)
70
+ return this
71
+ }
72
+ onBackPressed(callback: () => void): this {
73
+ this._backButtonCallback = callback
74
+ return this
75
+ }
76
+ }
77
+
78
+ /** Create a video node showing `player`'s output. */
79
+ export function UIVideo(player: VideoPlayer): UIVideo
80
+ /** @deprecated Style as the first argument is legacy — use chained `.style({...})`. */
81
+ export function UIVideo(style: UIVideoStyle, player: VideoPlayer): UIVideo
82
+ export function UIVideo(...args: [VideoPlayer] | [UIVideoStyle, VideoPlayer]): UIVideo {
83
+ if (args.length === 1) {
84
+ return new VideoElement({}, args[0])
85
+ } else {
86
+ return new VideoElement(args[0], args[1])
87
+ }
88
+ }
@@ -0,0 +1,241 @@
1
+ import { Element, type Color, type DrawableStyle, type ElementStyle, type UIElementBase, type UINodeChild } from "./UINode"
2
+
3
+ export type UIVirtualizedListStyle = DrawableStyle & ElementStyle & {
4
+ /** Tint of the native pull-to-refresh spinner (pairs with `onRefresh`). */
5
+ refreshControlColor?: Color,
6
+ /** Same key and semantics as UIScrollable's; `"none"` keeps a chat composer's keyboard up no
7
+ * matter how the history is scrolled. */
8
+ keyboardDismissMode?: "interactive" | "scroll" | "none",
9
+ }
10
+
11
+ export interface VListConfig<T> {
12
+ /** Stable unique key of an item. */
13
+ keyOf: (item: T) => string
14
+ /** Subtree factory — called when the item enters the window; the subtree lives until it
15
+ * leaves. */
16
+ render: (item: T) => UINodeChild
17
+ /** Height estimate (px) used before the first measurement — a number, or a per-item function
18
+ * (chat: `msg => 40 + Math.ceil(msg.text.length / 35) * 20`). Computed once when the item is
19
+ * added. */
20
+ estimatedHeight: number | ((item: T) => number)
21
+ /** Extra px around the viewport kept mounted. Default: one viewport height. */
22
+ overscan?: number
23
+ /** Chat mode: the first layout starts scrolled to the end, and appends auto-scroll while the
24
+ * list sits at the bottom. */
25
+ inverted?: boolean
26
+ }
27
+
28
+ /** A windowed (virtualized) list: only the items near the viewport are mounted — for long feeds
29
+ * and chats. Items are keyed; feed it via `setData`/`append`/`prepend`. */
30
+ export interface UIVirtualizedList<T = unknown> extends UIElementBase<UIVirtualizedListStyle> {
31
+ readonly type: "vlist"
32
+
33
+ /** Replace the data wholesale — vanished keys are unmounted by the host's window sync. */
34
+ setData(items: T[]): this
35
+ /** Append items to the end (auto-scrolls when `inverted` and resting at the bottom). */
36
+ append(...items: T[]): this
37
+ /** Insert at the start WITHOUT a scroll jump (the host compensates the offset). */
38
+ prepend(...items: T[]): this
39
+ /** Update items with the same keys: mounted ones re-render; the cached height persists until
40
+ * re-measured. */
41
+ update(...items: T[]): this
42
+ /** Remove items by key. */
43
+ removeByKey(...keys: string[]): this
44
+
45
+ /** Number of data items (mounted or not). */
46
+ readonly itemCount: number
47
+ /** The data item under `key`, if any. */
48
+ getItem(key: string): T | undefined
49
+
50
+ /** Scroll to `offset` px. */
51
+ scrollTo(offset: number, animated?: boolean): void
52
+ /** Scroll to the item with `key`. */
53
+ scrollToKey(key: string, animated?: boolean): void
54
+ /** Scroll to the end of the content. */
55
+ scrollToEnd(animated?: boolean): void
56
+
57
+ /** Scroll position changed (px from the start edge). */
58
+ onScroll(callback: (scrollPosition: number) => void): this
59
+ /** Pull-to-refresh: the spinner stays until the returned promise settles. Attach BEFORE the
60
+ * node mounts (hosts read it at creation). Native hosts only — no-op on web. */
61
+ onRefresh(callback: () => void | Promise<void>): this
62
+ /** Fires within `thresholdPx` of the content's bottom edge — once, re-armed after leaving the
63
+ * edge (host-side latch). Infinite-scroll loading. */
64
+ onEndReached(thresholdPx: number, callback: () => void): this
65
+ /** Same, for the top edge — chat history loading. */
66
+ onStartReached(thresholdPx: number, callback: () => void): this
67
+ }
68
+
69
+ export class VListElement<T> extends Element<"vlist"> implements UIVirtualizedList<T> {
70
+
71
+ // --- read by native side in createUINode (copyProperty / copyNodePtr) ---
72
+ _overscan: number
73
+ _inverted: boolean
74
+ _keys: string[] = []
75
+ _estimates: number[] = []
76
+ _endThreshold = 0
77
+ _startThreshold = 0
78
+
79
+ // --- списки слушателей, нативка забирает через copyPropertyListPtr ---
80
+ protected sl: ((scrollPosition: number) => void)[] = [] // scroll
81
+ protected erl: (() => void)[] = [] // endReached
82
+ protected strl: (() => void)[] = [] // startReached
83
+
84
+ // --- приватное состояние JS-стороны ---
85
+ private items = new Map<string, T>()
86
+ private mounted = new Map<string, UINodeChild>()
87
+ private keyOf: (item: T) => string
88
+ private renderItem: (item: T) => UINodeChild
89
+ private estimate: (item: T) => number
90
+
91
+ constructor(config: VListConfig<T>) {
92
+ // flexShrink: 1 default (SDK-owned, same as UIScrollable): an explicit height/flexBasis
93
+ // larger than the available space shrinks to fit instead of overflowing. The vlist's own
94
+ // yoga basis is 0 (items live in detached roots), so this is inert until a size is set.
95
+ super("vlist", { flexShrink: 1 })
96
+ this.keyOf = config.keyOf
97
+ this.renderItem = config.render
98
+ this.estimate = typeof config.estimatedHeight === "function"
99
+ ? config.estimatedHeight
100
+ : () => config.estimatedHeight as number
101
+ this._overscan = config.overscan ?? -1
102
+ this._inverted = config.inverted ?? false
103
+ }
104
+
105
+ private get isAttached(): boolean {
106
+ return !!this._id && this._id !== 0
107
+ }
108
+
109
+ private register(items: T[]): [string[], number[]] {
110
+ const keys: string[] = []
111
+ const estimates: number[] = []
112
+ for (const item of items) {
113
+ const k = this.keyOf(item)
114
+ this.items.set(k, item)
115
+ keys.push(k)
116
+ estimates.push(this.estimate(item))
117
+ }
118
+ return [keys, estimates]
119
+ }
120
+
121
+ setData(items: T[]): this {
122
+ const prevItems = new Map(this.items)
123
+ const prevMounted = [...this.mounted.keys()]
124
+ this.items.clear()
125
+ const [keys, estimates] = this.register(items)
126
+ this._keys = keys
127
+ this._estimates = estimates
128
+ if (this.isAttached) {
129
+ _creatorUI.vlistSetKeys(this, keys, estimates)
130
+
131
+ for (const k of prevMounted) {
132
+ if (this.mounted.has(k) && this.items.get(k) !== prevItems.get(k)) {
133
+ _creatorUI.vlistInvalidate(this, k)
134
+ }
135
+ }
136
+ }
137
+ return this
138
+ }
139
+
140
+ append(...items: T[]): this {
141
+ const [keys, estimates] = this.register(items)
142
+ this._keys.push(...keys)
143
+ this._estimates.push(...estimates)
144
+ if (this.isAttached) {
145
+ _creatorUI.vlistInsertKeys(this, -1, keys, estimates)
146
+ }
147
+ return this
148
+ }
149
+
150
+ prepend(...items: T[]): this {
151
+ const [keys, estimates] = this.register(items)
152
+ this._keys.unshift(...keys)
153
+ this._estimates.unshift(...estimates)
154
+ if (this.isAttached) {
155
+ _creatorUI.vlistInsertKeys(this, 0, keys, estimates)
156
+ }
157
+ return this
158
+ }
159
+
160
+ update(...items: T[]): this {
161
+ for (const item of items) {
162
+ const k = this.keyOf(item)
163
+ if (!this.items.has(k)) continue
164
+ this.items.set(k, item)
165
+ if (this.isAttached && this.mounted.has(k)) {
166
+ _creatorUI.vlistInvalidate(this, k)
167
+ }
168
+ }
169
+ return this
170
+ }
171
+
172
+ removeByKey(...keys: string[]): this {
173
+ for (const k of keys) {
174
+ if (!this.items.delete(k)) continue
175
+ const i = this._keys.indexOf(k)
176
+ if (i >= 0) { this._keys.splice(i, 1); this._estimates.splice(i, 1) }
177
+ }
178
+ if (this.isAttached) {
179
+ _creatorUI.vlistRemoveKeys(this, keys)
180
+ }
181
+ return this
182
+ }
183
+
184
+ get itemCount(): number { return this.items.size }
185
+ getItem(key: string): T | undefined { return this.items.get(key) }
186
+
187
+ // --- команды: офсеты и позиции знает только нативка (префикс-суммы) ---
188
+ scrollTo(offset: number, animated = true): void {
189
+ _creatorUI.command(this, "scrollTo", offset, animated)
190
+ }
191
+ scrollToKey(key: string, animated = true): void {
192
+ _creatorUI.command(this, "scrollToKey", key, animated)
193
+ }
194
+ scrollToEnd(animated = true): void {
195
+ _creatorUI.command(this, "scrollToEnd", animated)
196
+ }
197
+
198
+ onScroll(callback: (scrollPosition: number) => void): this {
199
+ this.sl.push(callback)
200
+ return this
201
+ }
202
+ // Читается нативкой при маунте (тот же контракт, что у UIScrollable).
203
+ declare _refreshCallback: any
204
+ onRefresh(callback: () => void | Promise<void>): this {
205
+ this._refreshCallback = callback
206
+ return this
207
+ }
208
+ onEndReached(thresholdPx: number, callback: () => void): this {
209
+ this._endThreshold = thresholdPx
210
+ this.erl.push(callback)
211
+ return this
212
+ }
213
+ onStartReached(thresholdPx: number, callback: () => void): this {
214
+ this._startThreshold = thresholdPx
215
+ this.strl.push(callback)
216
+ return this
217
+ }
218
+
219
+ /** @internal The one host→JS callback (via jsPtr, like button). Fires ONLY when the window
220
+ * crossed a boundary — not per scroll frame. Order: unmounts first (the host already tore the
221
+ * subtrees down and zeroed _id; only JS refs are cleaned here), then mounts. */
222
+ _syncWindow(unmountKeys: string[], mountKeys: string[]): void {
223
+ for (const k of unmountKeys) {
224
+ this.mounted.delete(k)
225
+ }
226
+ for (const k of mountKeys) {
227
+ const item = this.items.get(k)
228
+ if (item === undefined) continue
229
+ const node = this.renderItem(item)
230
+ this.mounted.set(k, node)
231
+ // createUINode в detached yoga root + измерение (width, NaN) + кэш высоты
232
+ _creatorUI.vlistMount(this, k, node)
233
+ }
234
+ }
235
+ }
236
+
237
+ /** Create a virtualized list from a config (`keyOf` + `render` + `estimatedHeight` required),
238
+ * then feed it via `setData`/`append`/`prepend`. Give it a size (`flexGrow: 1` as a screen body). */
239
+ export function UIVirtualizedList<T>(config: VListConfig<T>): UIVirtualizedList<T> {
240
+ return new VListElement<T>(config)
241
+ }