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
package/prompts/ui.md ADDED
@@ -0,0 +1,803 @@
1
+ // ===== UI RULES =====
2
+ // - Default screen background is BLACK — always set bgColor and text color explicitly
3
+ // - Only use style properties listed below — never invent new ones, they may not exist in the engine
4
+ // - Extract repeated styles into Style<T> objects and repeated UI into factory functions
5
+ // - Keep layout minimal — no wrapper containers that exist only to align something (alignment is a container property)
6
+
7
+ // ===== UI COMPONENTS =====
8
+ // Every element is created by a global factory function (never `new`) that takes only the element's
9
+ // CONTENT — children as plain arguments (or text/src/...). Everything else — styles, handlers — is
10
+ // configured by chaining: every configuring method returns the element itself, so construction reads
11
+ // as one chain.
12
+ UIColumn(UIText("Title"), UIButton(UIText("Go")))
13
+ // An ARRAY argument is flattened into the children — pass items.map(Row) directly, no spread:
14
+ UIColumn(header, items.map(Row), footer)
15
+
16
+ // UIRow, UIColumn — containers (UIColumn stacks vertically, UIRow horizontally)
17
+ UIRow(...children) / UIColumn(...children)
18
+ // Child management — imperative, no diffing; works before and after the element is on screen:
19
+ // .append(...nodes), .insert(index, ...nodes), .remove(...nodes), .setContent(nodes), .children (readonly)
20
+ // .setContent is the "re-render" primitive — build a fresh array (items.map(Row)) and swap it in.
21
+ // For long or unbounded data use UIVirtualizedList instead of setContent over a big array.
22
+
23
+ // UIScreen — root screen, always fills the device. Behaves as a UIColumn.
24
+ UIScreen(...children)
25
+ // .open() / .close() — show/close directly (single-screen apps; open() while a Router is active hides the router)
26
+ // .onOpen(cb), .onClose(cb) — fire on EVERY activation, not just the first: Router.push away fires onClose,
27
+ // popping back fires onOpen again. Anything started in onOpen (loops, intervals, sockets) MUST be stopped
28
+ // in onClose (see TIMERS & FRAME LOOP above).
29
+ // .onTouchStart(cb) — fires for touches anywhere on the screen; use for full-screen gestures (see TOUCH GESTURES above)
30
+ // .onBackPressed(cb) — Android hardware/gesture back; typically Router.pop()
31
+ // Screens NEVER scroll — the canonical screen is fixed chrome (header, tab bar) + ONE UIScrollable body
32
+ // with flexGrow: 1: UIScreen(Header(), UIScrollable(content).style({ flexGrow: 1 }))
33
+ // Note: a screen always fills the device — sizing styles on it (width, height, flexGrow, flexShrink, position) are no-ops
34
+
35
+ // UITabs — THE bottom-tab app shell: swipeable tabs (a UIPager) + a themed tab bar, as one UIScreen.
36
+ // USE THIS for every tabbed app — never hand-build a tab bar. Keys are tab ids, in tab order:
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) // UITabs IS a UIScreen — present it directly
42
+ // .select(id), .tab (getter), .onSelect(cb(id, i)) — fires on a bar tap, swipe, or select()
43
+ // .badge(id, value) — true = dot, number/string = count pill, false/null/0 clears
44
+ // .pager — the UIPager underneath; UIPager.push(detail) from any screen keeps the bar
45
+ // Styling is THEME-driven: theme({ primaryColor, mutedColor, tabbarBg, tabbarBorder, badgeColor })
46
+ // restyles the bar app-wide (dark fallbacks built in). For a custom bar layout use UIPager below.
47
+
48
+ // UIPager — the navigation primitive under UITabs: sibling tabs that swipe natively, each tab its
49
+ // OWN push/pop stack. Reach for it directly for a plain stack (one-screen pager) or a fully custom
50
+ // tab bar. Renders no bar — build your own next to it; give the pager flexGrow: 1.
51
+ UIPager(...tabs) // the tab root screens (arrays flatten); tabs are FIXED at construction
52
+ // .select(i, animated?) (instant by default), .index, .onSelect(cb(i)) — fires for taps AND swipes: sync the
53
+ // bar highlight here. Tabs keep their stack/scroll state when switched away and back.
54
+ // .push(screen) — slides onto the CURRENT tab; edge back-swipe / Android back pops natively.
55
+ // .pop(), .popToRoot(), .replace(screen), .depth (tab swiping is disabled while > 1), .onChange(cb(depth))
56
+ // Ambient from any screen, no reference needed: UIPager.push(screen) / UIPager.pop() / UIPager.current
57
+ // pager.push = detail INSIDE the tab (bar stays); Router.push = above the whole shell (bar covered).
58
+ // Note: tab screens are built up front, but onOpen fires only when the tab becomes visible (maybe never) —
59
+ // load initial data at build time, keep onOpen for re-entry.
60
+ const pager = UIPager(homeTab, searchTab, profileTab).style({ flexGrow: 1 })
61
+ Router.init(UIScreen(pager, tabBar)) // bar buttons: .onClick(() => pager.select(i))
62
+
63
+ // UIWidget — floating overlay, independent of screens, always position: fixed in device coordinates
64
+ // Persists across Router navigation — create ONCE at module scope, reuse; hidden by default.
65
+ UIWidget(...children)
66
+ // .show(), .hide(), .isShow (getter), .onTouchStart(cb), .onBackPressed(cb)
67
+ // extra style: overlayColor — full-screen scrim BEHIND the widget that blocks taps underneath, turning it into
68
+ // a modal; null (default) = no layer, "transparent" = invisible but still blocks. A scrim tap fires
69
+ // .onOverlayTap(cb) — usually () => widget.hide()
70
+ // If a widget belongs to one screen only, pair it with that screen's lifecycle:
71
+ // screen.onOpen(() => sheet.show()).onClose(() => sheet.hide())
72
+ // Exit animation: .animateTo({ ..., commit: false }) then .hide() — see animateTo below.
73
+
74
+ // UIModal — a UIWidget prewired as a dialog: USE THIS for confirm/alert dialogs
75
+ // Scrim on by default (overlayColor "rgba(0,0,0,0.5)"), animated show/hide (200ms fade), scrim tap and
76
+ // back button close it automatically. Create ONCE at module scope, like any widget.
77
+ UIModal(...children)
78
+ // .show(), .hide() (animated on a modal), .isOpen (getter), .onOpen(cb), .onClose(cb), .dismissible(false)
79
+ // — plus the full UIWidget surface
80
+ // .transition(hidden) — replace the show/hide animation: `hidden` is the off-screen pose (show animates FROM
81
+ // it, hide TO it; its duration times both), e.g. .transition({ transform: "translateY(480px)", duration: 250 })
82
+ // for a slide. Px only — no % in transform.
83
+ // Position/size it like a widget (left/right/top/...); style the box (bgColor, borderRadius, p)
84
+
85
+ // UIBottomSheet — a UIModal pinned to the bottom edge: USE THIS for every bottom sheet, never hand-build one
86
+ // Content-sized by default (as tall as its children, capped at the screen) — one position, drag down to
87
+ // dismiss: the action-sheet shape. Native hosts own the drag (snap, velocity, scroll handoff); web shows it static.
88
+ UIBottomSheet(...children)
89
+ // .detents([0.3, 0.6, 1]) — snap positions, ascending fractions of screen height (the map-app model). Call
90
+ // BEFORE show(); sizes the sheet to the HIGHEST detent — lower detents show the top slice of the content.
91
+ // .setDetent(i) (animated), .detent (getter), .onDetentChange(cb(i)) — every settle: finger snap or setDetent()
92
+ // .show()/.hide() slide in/out — plus the full UIModal surface (scrim, onOpen/onClose, dismissible).
93
+ // Dragging below the lowest detent closes it (fires onClose); .dismissible(false) collapses there instead —
94
+ // the persistent map sheet (pair it with overlayColor: null so the page behind stays interactive).
95
+ // An inner UIScrollable just works: it scrolls at the top detent; pulling down from its top drags the sheet.
96
+
97
+ // UIPopover — a UIModal prewired as an ANCHORED menu: USE THIS for dropdowns, context menus, tooltips
98
+ // Transparent intercepting scrim (outside tap dismisses; content under it can't scroll), 120ms fade,
99
+ // automatic placement: below the anchor, flips above near the bottom edge, clamped into the viewport.
100
+ // Attaches itself to Presentable.current (hides with the page it opened on).
101
+ UIPopover(...children)
102
+ // .show(anchor?) — anchor: any element, or { x: ev.clientX, y: ev.clientY } for long-press context menus
103
+ // .hide() — plus the full UIModal surface (isOpen, onOpen/onClose, dismissible, transition)
104
+ // Style the menu box yourself (width, bgColor, borderRadius); do NOT set left/top — show(anchor) owns them.
105
+
106
+ // UIScrollable — THE scroll container: a screen's scrolling body, a list under a pinned header, a carousel
107
+ UIScrollable(...children)
108
+ // extra styles: scrollDirection ("horizontal" | "vertical", default vertical), showScrollbar: boolean,
109
+ // overscrollMode ("none" | "absorb" | "default"), refreshControlColor — tints the pull-to-refresh spinner,
110
+ // keyboardDismissMode ("interactive" | "scroll" | "none") — "scroll": any drag dismisses the keyboard at once (search lists)
111
+ // snap ("none" | "start" | "center" | "end", default "none") — paging: a released drag settles on a
112
+ // direct child's boundary; the value picks where the child rests in the viewport. Snap targets are
113
+ // the children themselves, so item widths can differ. (Mobile hosts; web degrades to free scrolling.)
114
+ // .onScroll(cb(pos)), .onScrollRelease(cb), .onOverscroll(cb(delta))
115
+ // .onRefresh(async cb) — pull-to-refresh; spinner stays until the returned promise settles. Attach BEFORE the
116
+ // element mounts; vertical only; native hosts (web preview: no-op). UIVirtualizedList has the same contract.
117
+ // Note: NO programmatic scrolling (no scrollTo) — if you need scrollTo/scrollToEnd, use UIVirtualizedList
118
+ // Note: defaults flexShrink: 1 (scrolls instead of overflowing); wrapping ancestors still need flexShrink: 1
119
+ // themselves. flexGrow: 1 to fill the remaining space is still yours to set.
120
+ // Carousel = scrollDirection: "horizontal" + FIXED-width cards + snap: "start" ("center" for a card-deck-with-peek)
121
+
122
+ // UIVirtualizedList<T> — windowed list for LONG or unbounded data (feeds, chats, search results): only the
123
+ // visible rows (plus a buffer) are mounted. Use it instead of UIScrollable + map() whenever the item count
124
+ // is large, grows over time, or is unknown.
125
+ UIVirtualizedList<T>({
126
+ keyOf: (item: T) => string, // STABLE unique id per item (never the array index)
127
+ render: (item: T) => UINodeChild, // builds one row; called lazily as rows enter the window
128
+ estimatedHeight: number | ((item: T) => number), // px guess per row (real height measured after mount)
129
+ overscan?: number, // extra px mounted above/below the viewport (default: one viewport)
130
+ inverted?: boolean, // true = chat mode: starts scrolled to the end, append at bottom auto-scrolls
131
+ })
132
+ // Data is driven IMPERATIVELY (no diffing) — call these, never re-create the list:
133
+ // .setData(items: T[]) // replace everything (diffed by key)
134
+ // .append(...items) / .prepend(...items) // variadic; append = newest chat msg; prepend = history, no scroll jump
135
+ // .update(...items) // re-render rows with the same keyOf(); unknown keys ignored
136
+ // .removeByKey(...keys)
137
+ // .itemCount (getter) / .getItem(key)
138
+ // .scrollTo(offset, animated?) / .scrollToKey(key, animated?) / .scrollToEnd(animated?) / .onScroll(cb(pos))
139
+ // .onEndReached(thresholdPx: number, cb) // threshold FIRST and required; fires near the BOTTOM — load next page
140
+ // .onStartReached(thresholdPx: number, cb) // near the TOP (chat history)
141
+ // .onRefresh(async cb) // pull-to-refresh, same contract as UIScrollable (attach before mount)
142
+ // Edge callbacks are latched — fire once entering the threshold zone, re-arm when scrolled back out;
143
+ // still keep your own busy/end flags for paging.
144
+ // extra styles: refreshControlColor + element styles — no intrinsic height: give it flexGrow: 1 (or an explicit height) or it collapses to 0
145
+ // Note: render() must be pure over the item — a row may be unmounted and re-rendered at ANY time, so its output
146
+ // can't depend on outside mutable state. To change a mounted row, change the item and call .update(item).
147
+ // estimatedHeight only needs to be close — accurate guesses just reduce scroll jitter.
148
+
149
+ // UIText
150
+ UIText(text)
151
+ // .text (get/set) — mutating re-renders immediately
152
+ // extra styles: fontSize (default 14), fontWeight (number | "normal" | "bold"), fontFamily, color,
153
+ // textAlign ("start"|"center"|"end"|"left"|"right", default left), lineHeight, fontStyle ("normal"|"italic"),
154
+ // textDecoration ("underline"|"line-through"|"none"), letterSpacing
155
+
156
+ // UIImage
157
+ UIImage(src) // src: string url | FetchResponse | File | SvgSource | Canvas (see CANVAS section —
158
+ // charts/generated graphics; redraw + canvas.update() refreshes the shown image)
159
+ // .src (get/set) — swaps the displayed image
160
+ // extra styles: objectFit ("cover"|"contain"|"fill"), tintColor (SVG only), borderRadius (number px only on UIImage)
161
+ // .setSourceRect(x, y, w, h) — crop to a sub-rect of the source (texture px); frame fills the box, objectFit
162
+ // ignored. Chainable, safe to call before mount. For atlases/spritesheets — swap the rect to change frame.
163
+ // Note: tintColor applies only to SVG sources — it replaces ALL fill and stroke colors (monochrome icons only)
164
+ // Note: a bare relative path won't resolve — plain strings are https:// URLs; project files via import or asset('./photo.png')
165
+
166
+ // UIVideo — the on-screen surface only; playback lives entirely on the VideoPlayer (see AUDIO / VIDEO above)
167
+ UIVideo(player)
168
+ // .player (readonly) — the VideoPlayer passed at construction
169
+ // extra styles: objectFit ("cover"|"contain"|"fill")
170
+ const player = new VideoPlayer(src)
171
+ const video = UIVideo(player).style({ width: "100%", height: 220, objectFit: "cover" })
172
+ player.play() // stop playback in the screen's onClose; .dispose() when gone for good
173
+
174
+ // UIButton — the only TAPPABLE container: a UIRow with children centered on both axes by default.
175
+ // To make anything clickable (a card, a list row, an icon) — wrap it in a UIButton.
176
+ UIButton(...children)
177
+ // .onClick(cb), .onTouchStart(cb), .isPressed() // gestures: see TOUCH GESTURES above
178
+ // extra styles: onPressed: { bgColor, opacity, ..., duration? } — style while the finger is down;
179
+ // rippleColor ("default" or a Color, Android only)
180
+ // Note: press feedback is OPT-IN — nothing happens visually unless you set it. rippleColor replaces the
181
+ // onPressed visual on Android, so rippleColor + onPressed = ripple on Android, onPressed dim on iOS.
182
+ // Note: buttons render no chrome of their own — style bgColor/borderRadius/padding yourself, and give a
183
+ // button an explicit height (on its own it is only as tall as its text). flexDirection: "column" for cards.
184
+
185
+ // UIInput / UITextArea — single-line / multi-line text field
186
+ UIInput() / UITextArea()
187
+ // .value (get/set), .onChange(cb(value)), .onFocus(cb), .onBlur(cb), .focus(), .blur()
188
+ // .onSubmit(cb(value)) — keyboard return key (UIInput ONLY; in UITextArea Enter is a newline)
189
+ // extra styles: placeholder, placeholderColor, plus text styles (fontSize, color, ...)
190
+ // type: "text"|"password"|"search"|"phone"|"email"|"number"|"decimal"|"url"|"date"|"time"
191
+ // enterKey: "done"|"go"|"next"|"search"|"send" (UIInput only) — return-key label
192
+ // maxLength: number; autocapitalize: "none"|"words"|"sentences"|"characters"; autocorrect: boolean
193
+ // keyboardShrink: boolean (default true) — false: keyboard OVERLAYS the UI, no relayout (chat composers)
194
+ // keyboardDismiss: boolean (default true) — false: taps never dismiss the keyboard (chat composers)
195
+ // onFocused: { borderColor, ..., duration? } — style while focused (like onPressed)
196
+ // Note: type values are KEYBOARD HINTS, not validators — "number" doesn't block pasted letters. The
197
+ // phone-pad value is "phone" (there is NO "tel").
198
+ // Note: "date"/"time" open the native PICKER in the keyboard slot (typing disabled). .value and onChange stay
199
+ // canonical ("YYYY-MM-DD" / "HH:MM" 24h) while the field displays localized — set/compare canonical only.
200
+ // Note: on submit the keyboard dismisses UNLESS enterKey is "next" — then call next.focus() in onSubmit:
201
+ // name.style({ enterKey: "next" }).onSubmit(() => email.focus())
202
+ // Note: keyboard dismissal is automatic (tap on a NON-INTERACTIVE area; return key; iOS Done bar on number
203
+ // pads). Taps on buttons/other inputs NEVER dismiss (the control acts, focus stays), and scrolling doesn't
204
+ // either unless the scrollable opts in via keyboardDismissMode — so dropdowns/autocomplete under a focused
205
+ // input just work. The host scrolls the focused field into view — write NO keyboard code beyond the
206
+ // enterKey: "next" chain (and .blur(); keyboardDismiss: false on a chat composer).
207
+ // Note: an input collapses to its placeholder's width and its text's height — see SIZING below.
208
+
209
+ // app.keyboardHeight (logical px, 0 hidden) + app.addEventListener("keyboard", cb(height, durationMs)) —
210
+ // the keyboard's RAW overlap with the viewport. Pair with keyboardShrink: false for a chat composer:
211
+ // app.addEventListener("keyboard", h => { composer.style.pb = Math.max(24, h) })
212
+
213
+ // UISpacer — flexible empty space (defaults flexGrow: 1), eats free space along the main axis.
214
+ // Only when plain alignment can't express it (one item pushed to the far end while the rest stay put):
215
+ UIRow(title, UISpacer(), closeButton)
216
+ // If ALL children move together, justifyContent ("space-between", "flex-end", ...) does it with no extra element.
217
+
218
+ // @deprecated UIBox — legacy centered container: defaults justifyContent AND alignItems to "center"
219
+ // (column direction). Kept for old projects — write new code with UIRow/UIColumn + explicit alignment.
220
+
221
+ // ===== STYLING =====
222
+ // Every UI element has a .style() method. It MERGES the given styles (does not replace) and RETURNS the
223
+ // element, so calls chain and later calls only override the keys they mention:
224
+ UIText("Hi").style({ color: "white" }).style({ fontSize: 20 }).onClick(...)
225
+
226
+ // Ways to set styles — pick the right one:
227
+ // 1. .style({...}) → declarative merge, the default. Use this 99% of the time.
228
+ // 2. el.style.foo = bar → direct single-property mutation after creation (hot paths),
229
+ // e.g. el.style.transform = `translateY(${y}px)`; el.style.foo reads it back.
230
+ // 3. .animateTo({..., duration, delay?, commit?, loop?}) → tween current → given values (duration in MS).
231
+ // Targets are COMMITTED into the style immediately; pass commit: false to play without persisting —
232
+ // the exit-animation pattern (fade an overlay, then .hide(); next show() starts from the intact style).
233
+ // loop: true | n repeats the tween — loopMode: "ping-pong" (default: there and back) or "restart"
234
+ // (snap back + replay — full-turn spinners via transform: "rotate(360deg)", shimmers). A looping
235
+ // animation is an effect, not a state change: it never commits, delay applies once, and it stops on
236
+ // the element's next animateTo/animateFrom or when it leaves the screen. (web/iOS/desktop; Android plays once until its next runtime.)
237
+ // 4. .animateFrom({..., duration, delay?}) → snap to given values, animate back to current (fade-in).
238
+ // Never modifies the stored style.
239
+ // For free-value tweens use animate() — see ANIMATION above.
240
+ // Older code may pass a style object as the FIRST factory argument — legacy; write new styles with .style().
241
+
242
+ // Mutable CONTENT properties are NOT styles — they live on the element:
243
+ // UIText: .text UIInput/UITextArea: .value UIImage: .src UIButton: .isPressed()
244
+ myText.text = "Updated" // re-renders immediately
245
+
246
+ // Style<T> is a global helper type for reusable style objects, no import needed:
247
+ const btn: Style<UIButton> = { bgColor: "#333", borderRadius: 12 }
248
+
249
+ // --- Style vocabulary ---
250
+ // Layout is Flexbox-only (Yoga engine). No CSS grid, no block layout.
251
+ // Every element is a flex container, position: relative, flexDirection: column by default (UIRow AND UIButton flip to row).
252
+ // Container layout: flexDirection, justifyContent ("flex-start"|"center"|"flex-end"|"space-between"|"space-evenly"),
253
+ // alignItems ("flex-start"|"center"|"flex-end"|"stretch", default "stretch"), gap, flexWrap
254
+ // Flex child: flex (flex: 1 ≡ grow 1 + shrink 1 + basis 0 — see SIZING), flexGrow (default 0),
255
+ // flexShrink (default 0), flexBase (NOT flexBasis), alignSelf, aspectRatio
256
+ // Size: width, height, minWidth, maxWidth, minHeight, maxHeight
257
+ // Position: position ("relative"|"absolute"|"static"), top, left, right, bottom
258
+ // Padding: p, px, py, pt, pb, pl, pr (≡ padding, paddingHorizontal, paddingVertical, paddingTop, ...)
259
+ // Margin: m, mx, my, mt, mb, ml, mr — margins also accept "auto" (mx: "auto" centers a fixed-width element)
260
+ // Background (containers/screen/button/input/video; UIText/UIImage/UISpacer have bgColor only):
261
+ // bgColor, bgImage (string | FetchResponse | File), bgSize ("cover"|"contain"|"tile"), bgGradient
262
+ // bgGradient: a CSS linear-gradient() or radial-gradient() string, e.g. "linear-gradient(to top, rgba(0,0,0,0.7), transparent)"
263
+ // or "radial-gradient(circle at 50% 40%, #7B2FF7, #0a0a1a)". Comma-separate several gradients to stack them (first = on top).
264
+ // Layers paint bottom-to-top: bgColor → bgImage → bgGradient (gradient over image = text scrim).
265
+ // bgImage is decoration BEHIND children — an image that IS the content belongs in UIImage.
266
+ // Border: border ("1px solid #333" or bare width), borderWidth, borderColor,
267
+ // borderRadius (UIValue, or per-corner string "0 0 20 20"), per-side shorthands (borderTop, ...) and
268
+ // longhands (borderTopWidth, borderTopColor, borderTopLeftRadius, ...)
269
+ // Other: display ("none" | "flex"), opacity (0..1), transform (CSS string, e.g. `translateY(12px)` — great for
270
+ // gestures), overflow ("hidden" | "visible"), boxSizing ("border-box" | "content-box"),
271
+ // pointerEvents ("all" | "none" — "none" lets taps pass through)
272
+
273
+ // --- Values & units (UIValue) ---
274
+ // number (px) | "50%" | "50vw" | "50vh" | "50vmin" | "50vmax" | "1.5em" | "calc(...)" | "min(...)" | "max(...)" | "clamp(min, val, max)" | "var(--name)" / "var(--name, fallback)"
275
+ // Note: number value → logical px; string value → same semantics as on the web
276
+ // Note: "em" resolves against the element's OWN fontSize (default 14) — there is NO style inheritance
277
+ // Note: calc()/min()/max()/clamp() combine px, viewport units, em, safe-area keywords, and nest freely —
278
+ // but do NOT support % ("calc(100% - 20px)" is invalid; use "calc(100vw - 20px)")
279
+
280
+ // --- Colors in UI styles ---
281
+ // hex "#f33"/"#f33c"/"#ff3333"/"#ff3333cc", packed int 0xff3333, "rgb(...)"/"rgba(...)", and exactly these names:
282
+ // white black red green blue yellow orange purple gray cyan magenta brown transparent clear
283
+ // (UI styles only — engine APIs are hex/packed-int only, see COLORS above)
284
+
285
+ // --- Safe areas & comfort ---
286
+ // Env keywords resolving to the device insets (notch, home indicator):
287
+ // "safe-top" | "safe-bottom" | "safe-left" | "safe-right"; "safe-all" — p / m shorthands only
288
+ // Comfort tokens — where content comfortably starts, never collapse to 0: max(safe-edge, knob) on clearance
289
+ // insets (iOS notch/home indicator, gesture nav); past exact-height system BARS (Android status/nav bar) the
290
+ // knob ADDS instead. Knob defaults: 12 top, 4 bottom, 16 horizontal (the page gutter):
291
+ // "comfort-top" | "comfort-bottom" | "comfort-left" | "comfort-right"
292
+ // "comfort-x" — px/mx | "comfort-y" — py/my | "comfort-all" — p / m
293
+ // Accepted on per-side padding & margin, position offsets (top/left/bottom/right), and inside calc()/min()/max():
294
+ .style({ pb: "comfort-bottom" }) // bottom bar: 34 over a home indicator, 52 above Android's button bar, 4 on inset-less devices
295
+ .style({ px: "comfort-x" }) // page gutter that also clears the notch in landscape
296
+ .style({ pb: "calc(comfort-bottom + 56px)" }) // scroll content clearing a 56px floating bar
297
+
298
+ // --- Theme variables ---
299
+ // theme({...}) merges app vars into one table; styles read them with "var(--name)" (numbers are px
300
+ // lengths, null removes a key). Re-calling re-styles the LIVE UI — dark mode is a second call.
301
+ // Returned accessors ARE the var() strings; system vars are statics (theme.primaryColor, …):
302
+ const T = theme({ primaryColor: "#0A84FF", "comfort-left": 20 })
303
+ label.style({ color: T.primaryColor, pl: "var(--comfort-left)" })
304
+ // SYSTEM DEFAULTS: theme({ color, fontFamily }) drive the DEFAULT text color/font — all text
305
+ // without explicit values follows (light theme = set once, not color:"black" per label).
306
+ // Bare comfort-* tokens apply the safe-area formula; "var(--comfort-top)" reads the raw knob.
307
+ // "var(--name, fallback)" applies the fallback while the key is unset. Env names (safe-*, vw/vh) are not theme keys.
308
+
309
+ // --- Responsive: onLandscape / onPortrait ---
310
+ // Any style can nest orientation overrides, merged on top while the device is in that orientation:
311
+ .style({ flexDirection: "column", p: 16, onLandscape: { flexDirection: "row", p: 32 } })
312
+ // Keys: onLandscape, onPortrait.
313
+
314
+ // --- Style classes ($name) — named states that CASCADE ---
315
+ // A $-prefixed key in .style() declares a named style state (like onPressed, but yours: selected,
316
+ // checked, expanded); duration/delay inside the block animate the swap. Drive it via el.class:
317
+ const item = UIButton(UIText("Wi-Fi")).style({ bgColor: "#151515", $selected: { bgColor: "#1d2b45", duration: 150 } })
318
+ item.onClick(ev => ev.target.class.selected = !ev.target.class.selected)
319
+ // el.class.selected reads/writes a boolean; el.class({ a: true, b: false }) is the chainable batch
320
+ // form; names work with or without the $. Class state persists across screen close/reopen.
321
+ // CASCADE: a class set on an element also activates same-name $ blocks on ALL its descendants —
322
+ // one toggle restyles a whole composite control, each part declaring its own reaction:
323
+ UIButton(
324
+ UIImage(icon).style({ tintColor: "#888", $selected: { tintColor: "#5b8cff" } }),
325
+ UIText("Label").style({ color: "#888", $selected: { color: "#5b8cff" } }),
326
+ ) // .class.selected = true → icon AND label restyle (the cascade stops at hosted screens/widgets)
327
+ // $pressed / $focused are RESERVED — the system toggles them (finger down / input focused) and they
328
+ // CASCADE, so a button's children can restyle during its press. onPressed/onFocused stay
329
+ // own-element-only — use those on nested interactives (a button inside a clickable card):
330
+ UIButton(UIText("Buy").style({ $pressed: { color: "#999" } })).style({ $pressed: { transform: "scale(0.97)" } })
331
+ // Precedence on the same prop: base < $classes < $pressed/$focused < onPressed/onFocused.
332
+
333
+ // --- Defaults that surprise ---
334
+ // flexShrink: 0 — elements don't shrink to fit (exception: UIScrollable, UIVirtualizedList and UIPager
335
+ // default flexShrink: 1 so they shrink/scroll instead of overflowing; wrapping ancestors still default to 0).
336
+ // flexGrow: 0 — nothing grows along the main axis without asking; no implicit min-sizes either.
337
+ // alignItems: "stretch" — children FILL the cross axis by default; set a size or alignSelf to opt out.
338
+ // overflow: "hidden" — children are clipped to the parent's box; set overflow: "visible" to let them escape.
339
+ // boxSizing: "border-box" — width/height include padding & border.
340
+ // Screen bg is black, default text is WHITE at fontSize 14 (retarget once via theme({ color, fontFamily })).
341
+ // On any light surface — white cards, sheets, inputs — white text renders invisible: use dark text there
342
+ // ("#111"/black), checking every UIText against the surface it actually sits on, not the screen.
343
+
344
+ // --- Screen size ---
345
+ // Units are logical px (iOS pt / Android dp), NOT physical pixels — fontSize: 16 looks the same on every device.
346
+ // Design for a phone canvas: width ~360–430 (use 390 as default), height ~670–930.
347
+ // Vertical space is scarce — SE-class screens run ~670, so long content goes in a UIScrollable body (screens never scroll).
348
+
349
+ // ===== REUSABLE COMPONENTS =====
350
+ // Extract repeated UI into factory functions — they return elements you can chain methods on:
351
+
352
+ const ClickableCard = (title: string, subtitle: string) => UIButton(
353
+ UIText(title).style({ fontWeight: 700, color: "white" }),
354
+ UIText(subtitle).style({ color: "#888", fontSize: 13 })
355
+ ).style({ px: 16, py: 12, gap: 4, flexDirection: "column", alignItems: "flex-start" })
356
+
357
+ // Use like any other element — chain after the call:
358
+ ClickableCard("Title", "Subtitle").style({ bgColor: "#111" }).onClick(() => ...)
359
+
360
+ // For shared styles use Style<T>:
361
+ const headingStyle: Style<UIText> = { color: "#ffffff", fontSize: 24, fontWeight: 700 }
362
+
363
+ // ===== CAPTURING ELEMENT REFERENCES =====
364
+ // Use an assignment expression right in the children — standard TypeScript:
365
+
366
+ let label: UIText
367
+ let input: UIInput
368
+
369
+ UIColumn(
370
+ label = UIText("Hello"), // = both assigns the variable AND adds the element to the column
371
+ input = UIInput(),
372
+ )
373
+
374
+ // Later:
375
+ label.text = "Updated"
376
+ input.value // read current value
377
+
378
+ // Prefer captured refs over .children[] — refs are typed; children entries need a cast
379
+ // ((button.children[0] as UIText).text vs buttonText.text).
380
+
381
+ // ===== CONDITIONAL CHILDREN =====
382
+ // A null / undefined / false child is skipped — no element, no layout slot.
383
+ UIColumn(header, isLoading ? spinner : null, showFooter && footer)
384
+ // Works for whole blocks too — a falsy argument is skipped, an array argument is flattened:
385
+ UIColumn(header, showList && items.map(Row))
386
+
387
+ // ===== SIZING: the two axes behave differently =====
388
+ // MAIN axis (row → width, column → height): elements stay as small as their content — nothing grows
389
+ // without flexGrow: 1 (no implicit min-sizes).
390
+ // CROSS axis: children fill the parent by default (alignItems defaults to "stretch"); set an explicit size
391
+ // or alignSelf to opt out.
392
+ // So UIRow(UIInput()) leaves the input at placeholder width — give it flexGrow: 1 to fill the row.
393
+ // EQUAL-width children (tab bars, button pairs): flex: 1 on each — it grows from a ZERO basis, so they end up
394
+ // equal. flexGrow: 1 alone splits only the LEFTOVER space on top of content-sized bases — the child with the
395
+ // longer label stays wider.
396
+ // Prefer giving UIInput and UIButton an explicit height — on their own they're only as tall as their text.
397
+
398
+ // ===== ELEMENT DIMENSIONS =====
399
+ // Sizes exist only after layout — no synchronous el.width getter.
400
+ // .onLayout(cb) — react to the element's measured box. Fires on first layout and whenever the
401
+ // box changes (resize, content change).
402
+ // layout: { width, height, top, left } // logical px; top/left RELATIVE TO THE PARENT
403
+ UIButton().onLayout(({ width }) => { buttonWidth = width })
404
+ // .getBoundingClientRect() — ABSOLUTE device-space rect on demand, like the web API: the same
405
+ // space UIWidget top/left position in, scroll offsets included. null before mount.
406
+ // { x, y, left, top, right, bottom, width, height }
407
+ // For dropdown/context menus DON'T hand-roll with this — UIPopover.show(anchor) does the whole
408
+ // anchoring (placement, flip, clamp, attach). Reach for the raw rect only for custom positioning.
409
+ // Position once at an interaction — never poll per frame.
410
+
411
+ // ===== ROUTER (multi-page apps) =====
412
+ // Tabs are UITabs' job, in-tab stacks UIPager's (see UI COMPONENTS) — the Router owns what sits ABOVE the shell.
413
+ Router.init(homeScreen, opts?: { showDefaultBackButton?: boolean }) // call once in the entry file (default false)
414
+ Router.push(screen) // push onto the stack, screen becomes active
415
+ Router.pop(to?: number) // default -1 = one back; negative = relative (-2 = back two), 0/positive = absolute
416
+ // stack index (0 = home). Popped screens' native trees are destroyed; the UIScreen
417
+ // object stays reusable (push remounts it).
418
+ Router.replace(screen, opts?: { transition }) // swap the top screen; transition: "slide-from-left" |
419
+ // "slide-from-right" | "slide-from-top" | "slide-from-bottom" | "zoom" | "zoom-out" | "zoom-in" | "fade" | "none" (default "fade")
420
+ Router.current // the active UIScreen (getter)
421
+ Router.hide() / Router.restore() // hide/restore the whole router — stack stays alive; fires the top screen's onClose/onOpen
422
+ Router.addEventListener("change", (screen: UIScreen) => ...) / .removeEventListener("change", cb) // after every navigation
423
+ // Screens BELOW the top stay mounted — element trees and state survive, they're just not rendered.
424
+ // push fires the outgoing screen's onClose + the incoming one's onOpen; pop fires them in reverse and
425
+ // re-fires onOpen on the screen you land on.
426
+
427
+ // ===== FONTS =====
428
+ // System fonts need no setup — use as fontFamily directly: "serif", "sans-serif", "monospaced".
429
+ // Custom fonts: the font() compile macro installs a font AND returns its family name. No loading
430
+ // code, no promises, no gating — just build the UI; the host has the faces ready at first paint.
431
+ theme({ fontFamily: font("manrope") }) // app-wide default text font
432
+ UIText("Big").style({ fontFamily: font("unbounded"), fontSize: 32 }) // or per node
433
+ font("ibm-plex-sans", { weights: [400, 600], italic: true }) // narrow/extend the installed faces
434
+ // Default weights are ~400/500/700 — install the weights you use (fontWeight: 600 wants a 600 face).
435
+ // The argument must be a string literal. Registry ids (all OFL, cyrillic-capable unless noted):
436
+ // sans: inter manrope golos-text onest rubik nunito montserrat ibm-plex-sans pt-sans jost exo-2
437
+ // display: unbounded russo-one oswald comfortaa space-grotesk(latin only)
438
+ // serif: playfair-display lora pt-serif cormorant literata
439
+ // mono: jetbrains-mono fira-code ibm-plex-mono handwriting: caveat pacifico
440
+ // A project's own file: font("./fonts/Brand.ttf") → real family name read from the file itself
441
+ // (weight/italic too — no options); two weight files of one family return the same name.
442
+ // registerFont(family, url, {weight, style}) survives ONLY for runtime-computed URLs — rare.
443
+ // No style inheritance — set fontFamily per UIText or via theme() (extract a shared Style<UIText>).
444
+
445
+ // ===== SVG IMAGES =====
446
+ // SvgSource wraps raw SVG XML for use as an image source: UIImage(SvgSource(`<svg ...>`)).
447
+ // Rendered by native nanosvg, not a browser — only basic geometry works (path, rect, circle, line, polygon,
448
+ // solid fill/stroke, opacity, transform, gradients). No text, filters or CSS — they vanish on device.
449
+ // Prefer a separate .svg file over inline XML — importing one yields a ready SvgSource (and gives the user a preview):
450
+ import heart from './assets/heart.svg' // or inline: asset('./assets/heart.svg')
451
+ UIImage(heart).style({ width: 24, height: 24, tintColor: "#666" }) // tintColor recolors the icon
452
+ // Keep inline SvgSource(`...`) only for SVG generated dynamically from data.
453
+
454
+ // ===== ICONS (assetIcon) =====
455
+ // Use real icons instead of emoji. assetIcon("pack:name") resolves the icon at COMPILE time and
456
+ // inlines it as an image source (same shape as SvgSource) — no imports, no project files, offline.
457
+ UIImage(assetIcon("lucide:bell")).style({ width: 24, height: 24, tintColor: "#8a8f98" })
458
+ // The id must be a string literal; an unknown id is a compile error (with name suggestions).
459
+ // Recolor via the tintColor style or the { color } option (hex literal bakes in; expression tints).
460
+ // Packs: lucide, tabler, heroicons, feather, bi, carbon, mdi, ri, solar.
461
+
462
+ // ===== COMMON MISTAKES — DO NOT DO THESE =====
463
+
464
+ // ❌ CSS that doesn't exist here
465
+ display: "grid" // WRONG — Flexbox only (Yoga)
466
+ calc(100% - 20px) // WRONG — calc() can't mix with %
467
+ lineHeight: 1.5 // WRONG — number is px (=1.5px); for a multiplier use "1.5em"
468
+
469
+ // ❌ touch handler on a non-touch element
470
+ UIColumn(...).onTouchStart(cb) // WRONG — only UIButton/UIScreen/UIWidget; wrap in UIButton
471
+
472
+ // ❌ let/const among the children — `let` is a statement, not an expression
473
+ UIRow(let input = UIInput()) // WRONG
474
+ // ✅ declare outside, assign inside (assignment both sets the var AND appends)
475
+ let input: UIInput
476
+ UIRow(input = UIInput())
477
+
478
+ // ❌ setting a UIImage's content through bgImage
479
+ const img = UIImage("") // WRONG — empty source as a placeholder
480
+ img.style.bgImage = url // WRONG — bgImage is a container background, not image content
481
+ // ✅ pass the source to the constructor, or swap it via .src
482
+ const img = UIImage(url)
483
+ img.src = newUrl // updates the displayed image
484
+
485
+ // ❌ expecting an input to fill width like in CSS
486
+ UIRow(UIInput()) // WRONG — collapses to placeholder width
487
+ // ✅ stretch it explicitly
488
+ UIColumn(UIInput().style({ width: "100%" })) // cross axis
489
+ UIRow(input = UIInput().style({ flexGrow: 1 }), sendBtn) // main axis
490
+
491
+ // ❌ a button that should match the input's height but shrinks to its text
492
+ UIRow(input.style({ height: 40 }), UIButton()) // button ends up shorter
493
+ // ✅ give controls the same explicit height
494
+ UIRow(input.style({ height: 40 }), UIButton().style({ height: 40 }))
495
+
496
+ // ❌ flexGrow: 1 for equal-width children — it splits only the LEFTOVER space, bases stay content-sized
497
+ UIRow(yes.style({ flexGrow: 1 }), no.style({ flexGrow: 1 })) // longer label = wider button
498
+ // ✅ flex: 1 — grows from a zero basis, children end up equal
499
+ UIRow(yes.style({ flex: 1 }), no.style({ flex: 1 }))
500
+
501
+ // ❌ empty containers as spacers to align children (web habit)
502
+ UIRow(UIColumn().style({ flexGrow: 1 }), label) // WRONG
503
+ // ✅ alignment is a CONTAINER property, not an extra element
504
+ UIRow(label).style({ justifyContent: "flex-end" })
505
+
506
+ // ❌ empty element as a placeholder for a conditional child
507
+ UIRow(isGroup ? button : UIColumn()) // WRONG
508
+ // ✅ null is skipped in children — no phantom element
509
+ UIRow(isGroup ? button : null)
510
+
511
+ // ❌ pointing UIImage / bgImage at a project file by bare path — it won't resolve to the bundled asset
512
+ UIImage("./photo.png") // WRONG (a plain string works only for remote http(s) URLs)
513
+ // ✅ import the asset, or wrap its path in asset()
514
+ import photo from './photo.png'
515
+ UIImage(photo) // or: UIImage(asset('./photo.png'))
516
+
517
+ // ❌ callback-first edge callbacks on UIVirtualizedList
518
+ list.onEndReached(loadNextPage) // WRONG — threshold is the first argument, and required
519
+ // ✅ threshold (px from the edge) first
520
+ list.onEndReached(600, loadNextPage)
521
+
522
+ // ❌ re-creating a UIWidget or UIVirtualizedList to "re-render"
523
+ const openSheet = () => UIWidget(...).show() // WRONG — leaks a new widget every call
524
+ // ✅ create once at module scope; show()/hide() the widget, setData/append/update the list
525
+
526
+ // ===== EXAMPLES =====
527
+
528
+ // === EXAMPLE 1: Single-file app ===
529
+ <file name="main.ts">
530
+ let text: UIText
531
+ const screen = UIScreen(
532
+ text = UIText("Hello, world!").style({ mb: 16, fontWeight: 700, fontSize: 24, textAlign: "center" }),
533
+ UIText("Your name:").style({ textAlign: "center" }),
534
+ UIInput()
535
+ .style({ bgColor: "white", height: 40, px: 8, borderRadius: 8, color: "black" })
536
+ .onChange(str => {
537
+ text.text = `Hello, ${str}!`
538
+ })
539
+ ).style({ justifyContent: "center", p: 20, gap: 8 })
540
+
541
+ screen.open()
542
+ </file>
543
+
544
+ // === EXAMPLE 2: Theme tokens + fetched list with loading / error states ===
545
+ // A tokens module every screen imports, light-themed: theme({ color }) sets the default text
546
+ // color ONCE — no color: "#111" on every label.
547
+ <file name="tokens.ts">
548
+ const palette = {
549
+ bg: "#F4F6F5", card: "#FFFFFF", border: "#E4E8E6",
550
+ text: "#131A17", muted: "#606B65",
551
+ accent: "#15A34A", accentSoft: "#E7F6ED", onAccent: "#FFFFFF",
552
+ }
553
+ theme({ color: palette.text, primaryColor: palette.accent })
554
+ // Accessors ARE "var(--x)" strings — re-calling theme() with new values restyles the live app.
555
+ export const colors: { [K in keyof typeof palette]: string } = theme(palette)
556
+ export const font = { // type scale — spread into styles: .style({ ...font.h2 })
557
+ h2: { fontSize: 22, fontWeight: 700 }, bodyStrong: { fontSize: 16, fontWeight: 600 },
558
+ small: { fontSize: 14 }, tiny: { fontSize: 12, fontWeight: 500 },
559
+ }
560
+ </file>
561
+ <file name="main.ts">
562
+ import { colors, font } from './tokens'
563
+
564
+ type User = { id: number; name: string; email: string }
565
+
566
+ const Row = (u: User) => UIRow(
567
+ UIColumn(UIText(u.name[0]).style({ ...font.bodyStrong, color: colors.accent }))
568
+ .style({ width: 44, height: 44, borderRadius: 22, bgColor: colors.accentSoft,
569
+ justifyContent: "center", alignItems: "center" }),
570
+ UIColumn(
571
+ UIText(u.name).style({ ...font.bodyStrong }),
572
+ UIText(u.email).style({ ...font.small, color: colors.muted }),
573
+ ).style({ flexGrow: 1, flexShrink: 1, gap: 2, alignItems: "flex-start" }),
574
+ ).style({ alignItems: "center", gap: 12, bgColor: colors.card, borderRadius: 16,
575
+ border: `1px solid ${colors.border}`, p: 14 })
576
+
577
+ const Centered = (...children: UINodeChild[]) =>
578
+ UIColumn(children).style({ flexGrow: 1, justifyContent: "center", alignItems: "center", gap: 12 })
579
+
580
+ let body: UIColumn
581
+
582
+ const loadData = async () => {
583
+ body.setContent([Centered(UIText("Loading…").style({ color: colors.muted }))])
584
+ const res = await fetch("https://jsonplaceholder.typicode.com/users")
585
+ if (res.status !== 200) {
586
+ body.setContent([Centered(
587
+ UIText("Couldn't load users"),
588
+ UIButton(UIText("Retry").style({ color: colors.onAccent, fontWeight: 600 }))
589
+ .style({ height: 44, px: 24, borderRadius: 12, bgColor: colors.accent })
590
+ .onClick(() => loadData()),
591
+ )])
592
+ return
593
+ }
594
+ body.setContent(res.json<User[]>().map(Row)) // res.json is sync — no await
595
+ }
596
+
597
+ const screen = UIScreen(
598
+ UIText("Users").style({ ...font.h2, pb: 8, px: 16 }),
599
+ UIScrollable(
600
+ body = UIColumn().style({ flexGrow: 1, gap: 8 })
601
+ ).style({ flexGrow: 1, p: 16, pt: 0 }) // the ONE scrolling body — the screen itself never scrolls
602
+ .onRefresh(() => loadData()), // pull-to-refresh; attached before screen.open()
603
+ ).style({ bgColor: colors.bg, pt: "comfort-top" })
604
+ .onOpen(() => loadData())
605
+
606
+ screen.open()
607
+ </file>
608
+
609
+ // === EXAMPLE 3: Tabbed app — UITabs, font(), in-tab detail ===
610
+ <file name="home.ts">
611
+ const detailScreen = (name: string) => UIScreen(
612
+ UIButton(
613
+ UIImage(assetIcon("lucide:chevron-left")).style({ width: 20, height: 20, tintColor: "white" }),
614
+ UIText("Back")
615
+ ).style({ alignSelf: "flex-start", height: 32, gap: 4 }).onClick(() => UIPager.pop()),
616
+ UIText(name).style({ fontSize: 24, fontWeight: 700 }),
617
+ UIButton(
618
+ UIImage(assetIcon("lucide:heart")).style({ width: 18, height: 18, tintColor: "#8a919e", $fav: { tintColor: "#ff453a" } }),
619
+ UIText("Favorite").style({ color: "#8a919e", $fav: { color: "#ff453a" } })
620
+ ).style({ name: "fav", alignSelf: "flex-start", height: 36, px: 12, gap: 6, borderRadius: 18, bgColor: "#17181c", $fav: { bgColor: "#2a181a", duration: 150 } })
621
+ .onClick(ev => ev.target.class.fav = !ev.target.class.fav) // one toggle — the $fav blocks on icon + label light up too (cascade)
622
+ ).style({ p: 16, pt: "comfort-top", gap: 16, bgColor: "black" })
623
+
624
+ const Item = (name: string) => UIButton(UIText(name).style({ color: "white" }))
625
+ .style({ height: 52, px: 16, justifyContent: "flex-start", borderRadius: 12, bgColor: "#151515", onPressed: { opacity: 0.7 } })
626
+ .onClick(() => UIPager.push(detailScreen(name))) // in-tab push: tab bar stays, back-swipe pops
627
+
628
+ export const homeScreen = UIScreen(
629
+ UIText("Home").style({ fontSize: 28, fontWeight: 700, fontFamily: font("unbounded") }), // display face
630
+ UIScrollable(["Alpha", "Beta", "Gamma"].map(Item)).style({ flexGrow: 1, gap: 8 })
631
+ ).style({ p: 16, pt: "comfort-top", gap: 16, bgColor: "black" })
632
+ </file>
633
+ <file name="profile.ts">
634
+ export const profileScreen = UIScreen(
635
+ UIText("Profile").style({ fontSize: 28, fontWeight: 700, fontFamily: font("unbounded") })
636
+ ).style({ p: 16, pt: "comfort-top", bgColor: "black" })
637
+ </file>
638
+ <file name="main.ts">
639
+ import { homeScreen } from './home'
640
+ import { profileScreen } from './profile'
641
+
642
+ theme({ fontFamily: font("manrope") }) // app-wide default text font — one line, no loading code
643
+
644
+ const tabs = UITabs({
645
+ home: { label: "Home", icon: assetIcon("lucide:house"), screen: homeScreen },
646
+ profile: { label: "Profile", icon: assetIcon("lucide:user"), screen: profileScreen },
647
+ })
648
+ tabs.badge("profile", true) // notification dot on the tab
649
+ Router.init(tabs)
650
+ </file>
651
+
652
+ // === EXAMPLE 4: Form — field factory, return-key chain, validation, busy submit ===
653
+ // The keyboard needs NO code beyond the enterKey chain: the host scrolls the focused field into
654
+ // view and handles dismissal itself.
655
+ <file name="main.ts">
656
+ // Field anatomy: label above, styled input, a RESERVED message line below (minHeight — an
657
+ // appearing error never jumps the form). Typing in an errored field clears it.
658
+ const Field = (label: string, style: Style<UIInput> = {}) => {
659
+ const input = UIInput().style({ height: 50, px: 14, borderRadius: 12, bgColor: "#161A22",
660
+ border: "1px solid #262C3A", color: "white", placeholderColor: "#5A6272",
661
+ onFocused: { borderColor: "#4C8DFF" }, ...style })
662
+ const message = UIText("").style({ fontSize: 13, minHeight: 18, color: "#FF6B6B" })
663
+ input.onChange(() => message.text = "")
664
+ return {
665
+ node: UIColumn(
666
+ UIText(label).style({ fontSize: 13, fontWeight: 600, color: "#8A93A6" }),
667
+ input, message,
668
+ ).style({ gap: 6 }),
669
+ input,
670
+ error: (text: string) => { message.text = text },
671
+ }
672
+ }
673
+
674
+ const name = Field("Name", { placeholder: "Jane Appleseed", autocapitalize: "words" })
675
+ const email = Field("Email", { type: "email", placeholder: "you@example.com" })
676
+ const password = Field("Password", { type: "password" })
677
+
678
+ // The return key walks the form; the last field submits. This is ALL the keyboard code.
679
+ name.input.style({ enterKey: "next" }).onSubmit(() => email.input.focus())
680
+ email.input.style({ enterKey: "next" }).onSubmit(() => password.input.focus())
681
+ password.input.style({ enterKey: "go" }).onSubmit(() => submit())
682
+
683
+ let btnLabel: UIText
684
+ let busy = false
685
+
686
+ // The submit button is never disabled — a tap on a bad form PAINTS the errors and focuses the
687
+ // first offender, which beats a dead button that explains nothing. `busy` swallows double-taps.
688
+ const submit = async () => {
689
+ if (busy) return
690
+ let bad: ReturnType<typeof Field> | null = null // checked bottom-up, so `bad` ends at the FIRST invalid field
691
+ if (password.input.value.length < 8) { password.error("At least 8 characters"); bad = password }
692
+ if (!email.input.value.includes("@")) { email.error("Enter a valid email"); bad = email }
693
+ if (name.input.value.trim() === "") { name.error("Name is required"); bad = name }
694
+ if (bad) { bad.input.focus(); return }
695
+ busy = true
696
+ btnLabel.text = "Creating…"
697
+ const res = await fetch("https://api.example.com/register", {
698
+ method: "POST", headers: { "Content-Type": "application/json" },
699
+ body: JSON.stringify({ name: name.input.value.trim(), email: email.input.value, password: password.input.value }),
700
+ })
701
+ busy = false
702
+ btnLabel.text = "Create account"
703
+ if (res.status !== 200) { email.error("Registration failed — try again"); return }
704
+ toast("Welcome!")
705
+ }
706
+
707
+ const screen = UIScreen(
708
+ UIScrollable( // no fixed chrome — the keyboard leaves ~460px of screen and a form wants all of them
709
+ UIText("Create account").style({ fontSize: 28, fontWeight: 700, mb: 12 }),
710
+ name.node, email.node, password.node,
711
+ UIButton(btnLabel = UIText("Create account").style({ fontSize: 16, fontWeight: 700 }))
712
+ .style({ name: "submit", height: 52, borderRadius: 14, bgColor: "#4C8DFF", mt: 8, onPressed: { opacity: 0.85 } })
713
+ .onClick(() => submit())
714
+ ).style({ flexGrow: 1, px: 20, pt: "comfort-top", pb: 28, gap: 8 })
715
+ ).style({ bgColor: "#0C0F14" })
716
+
717
+ screen.open()
718
+ </file>
719
+
720
+ // === EXAMPLE 5: UIBottomSheet — persistent map-style sheet with detents ===
721
+ <file name="main.ts">
722
+ type Place = { id: number; name: string; distance: string }
723
+ const places: Place[] = [
724
+ { id: 1, name: "Blue Bottle Coffee", distance: "120 m" },
725
+ { id: 2, name: "City Library", distance: "400 m" },
726
+ { id: 3, name: "Riverside Park", distance: "1.2 km" },
727
+ ]
728
+
729
+ const Row = (p: Place) => UIRow(
730
+ UIText(p.name).style({ color: "#111", fontSize: 16 }),
731
+ UIText(p.distance).style({ color: "#888", fontSize: 14 })
732
+ ).style({ px: 16, height: 52, alignItems: "center", justifyContent: "space-between" })
733
+
734
+ const sheet = UIBottomSheet(
735
+ UIColumn().style({ width: 50, height: 6, borderRadius: 3, bgColor: "#D9D9D9", mx: "auto", my: 12 }),
736
+ UIText("Nearby").style({ px: 16, fontWeight: 700, fontSize: 20, mb: 8, color: "black" }),
737
+ UIScrollable(places.map(Row)).style({ flexGrow: 1 }) // scrolls at the top detent, drags the sheet below it
738
+ )
739
+ .style({ bgColor: "white", borderRadius: 20, overlayColor: null }) // no scrim — the map stays interactive
740
+ .detents([0.25, 0.6, 1]) // collapsed / half / full
741
+ .dismissible(false) // drag below the lowest detent collapses, never closes
742
+ .onDetentChange(i => console.log("detent", i))
743
+
744
+ const mapScreen = UIScreen(
745
+ // the map / page content behind the sheet
746
+ ).onOpen(() => sheet.show()).onClose(() => sheet.hide())
747
+
748
+ mapScreen.open()
749
+
750
+ // An action sheet is even less: content-sized, no detents() — UIBottomSheet(rows).show(),
751
+ // scrim and drag-down-to-dismiss included.
752
+ </file>
753
+
754
+ // === EXAMPLE 6: UIVirtualizedList — chat (inverted, imperative append) ===
755
+ <file name="main.ts">
756
+ type Msg = { id: string; text: string; mine: boolean }
757
+
758
+ let nextId = 0
759
+ const idOf = () => String(++nextId)
760
+
761
+ // The list owns the rows imperatively — never re-create it, never map() over data into it.
762
+ const list = UIVirtualizedList<Msg>({
763
+ keyOf: m => m.id,
764
+ estimatedHeight: m => 44 + Math.ceil(m.text.length / 34) * 20,
765
+ inverted: true, // newest at the bottom
766
+ render: m => UIRow(
767
+ UIRow(
768
+ UIText(m.text).style({ color: m.mine ? "white" : "#111" })
769
+ ).style({
770
+ bgColor: m.mine ? "#FF4032" : "#EEE",
771
+ px: 12, py: 8, borderRadius: 16, maxWidth: "75%"
772
+ })
773
+ ).style({ px: 12, py: 4, justifyContent: m.mine ? "flex-end" : "flex-start" })
774
+ }).style({ flexGrow: 1 })
775
+
776
+ let input: UITextArea
777
+ let sendBtn: UIButton
778
+
779
+ const send = () => {
780
+ const text = input.value.trim()
781
+ if (!text) return
782
+ list.append({ id: idOf(), text, mine: true }) // O(1); inverted list auto-scrolls to it
783
+ input.value = ""
784
+ sendBtn.style.opacity = 0.4
785
+ }
786
+
787
+ const screen = UIScreen(
788
+ list,
789
+ UIRow(
790
+ input = UITextArea().style({ placeholder: "Message...", placeholderColor: "#999",
791
+ color: "#111", flexGrow: 1, flexShrink: 1, px: 16, py: 10, bgColor: "#F0F0F0", borderRadius: 20, maxHeight: 110,
792
+ keyboardDismiss: false
793
+ }),
794
+ sendBtn = UIButton(UIImage(assetIcon("lucide:arrow-up")).style({ width: 20, height: 20, tintColor: "white" }))
795
+ .style({ width: 40, height: 40, borderRadius: 20, bgColor: "#FF4032", opacity: 0.4 })
796
+ .onClick(send)
797
+ ).style({ p: 8, pb: "comfort-bottom", gap: 8, alignItems: "flex-end" })
798
+ ).style({ bgColor: "white" })
799
+
800
+ input.onChange(v => { sendBtn.style.opacity = v.trim() ? 1 : 0.4 }) // direct style write — the hot-path form
801
+
802
+ screen.open()
803
+ </file>