lecodes-sdk 1.2.0 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (260) hide show
  1. package/README.md +5 -2
  2. package/dist/editor.d.ts +12 -0
  3. package/dist/global.d.ts +4 -8
  4. package/dist/host.d.ts +2 -3
  5. package/dist/types/animate/tween/Animation.d.ts +0 -3
  6. package/dist/types/animate/tween/animateValue.d.ts +4 -2
  7. package/dist/types/animate/tween/easing.d.ts +8 -0
  8. package/dist/types/animate/tween/spec.d.ts +15 -7
  9. package/dist/types/audio/audio.d.ts +2 -1
  10. package/dist/types/canvas/Canvas.d.ts +40 -108
  11. package/dist/types/canvas/gen/cssColor.d.ts +17 -0
  12. package/dist/types/canvas/gen/recorder.d.ts +118 -0
  13. package/dist/types/canvas/gen/spec.d.ts +144 -0
  14. package/dist/types/core/color.d.ts +3 -1
  15. package/dist/types/core/pins.d.ts +18 -0
  16. package/dist/types/g2/Node2D.d.ts +5 -8
  17. package/dist/types/g2/Scene2D.d.ts +6 -2
  18. package/dist/types/gl/Foliage.d.ts +30 -7
  19. package/dist/types/gl/Light.d.ts +8 -0
  20. package/dist/types/gl/Lightmap.d.ts +13 -2
  21. package/dist/types/gl/Material.d.ts +17 -3
  22. package/dist/types/gl/Model.d.ts +6 -2
  23. package/dist/types/gl/Node.d.ts +3 -6
  24. package/dist/types/gl/Scene.d.ts +41 -13
  25. package/dist/types/gl/Texture.d.ts +1 -1
  26. package/dist/types/gl/animation/Locomotion.d.ts +8 -1
  27. package/dist/types/inject.d.ts +11 -11
  28. package/dist/types/inject.editor.d.ts +1 -0
  29. package/dist/types/net/core.d.ts +7 -0
  30. package/dist/types/plugin.d.ts +76 -0
  31. package/dist/types/plugins/gen/camera/sdk/camera.d.ts +24 -0
  32. package/dist/types/plugins/gen/camera/sdk/camera.gen.d.ts +25 -0
  33. package/dist/types/plugins/{geolocation.d.ts → gen/geolocation/sdk/geolocation.d.ts} +2 -20
  34. package/dist/types/plugins/gen/geolocation/sdk/geolocation.gen.d.ts +31 -0
  35. package/dist/types/plugins/{map.d.ts → gen/map/sdk/map.d.ts} +9 -50
  36. package/dist/types/plugins/gen/map/sdk/map.gen.d.ts +53 -0
  37. package/dist/types/plugins/gen/push/sdk/push.d.ts +23 -0
  38. package/dist/types/plugins/gen/push/sdk/push.gen.d.ts +35 -0
  39. package/dist/types/plugins/{qr.d.ts → gen/qr-scanner/sdk/qr-scanner.d.ts} +2 -3
  40. package/dist/types/plugins/gen/qr-scanner/sdk/qr-scanner.gen.d.ts +15 -0
  41. package/dist/types/runtime/app.d.ts +9 -2
  42. package/dist/types/runtime/fetch.d.ts +2 -0
  43. package/dist/types/runtime/input.d.ts +1 -1
  44. package/dist/types/runtime/media.d.ts +6 -10
  45. package/dist/types/runtime/misc.d.ts +4 -1
  46. package/dist/types/runtime/net.d.ts +3 -2
  47. package/dist/types/runtime/touch.d.ts +32 -0
  48. package/dist/types/scene/defineScene.d.ts +43 -2
  49. package/dist/types/scene/editor.d.ts +52 -0
  50. package/dist/types/scene/gizmos.d.ts +7 -4
  51. package/dist/types/ui/NativeView.d.ts +6 -4
  52. package/dist/types/ui/UI.d.ts +1 -1
  53. package/dist/types/ui/UIBottomSheet.d.ts +6 -12
  54. package/dist/types/ui/UIButton.d.ts +12 -14
  55. package/dist/types/ui/UIContainer.d.ts +0 -6
  56. package/dist/types/ui/UIImage.d.ts +1 -4
  57. package/dist/types/ui/UIInput.d.ts +8 -24
  58. package/dist/types/ui/UIModal.d.ts +0 -2
  59. package/dist/types/ui/UINode.d.ts +70 -56
  60. package/dist/types/ui/UIPager.d.ts +28 -27
  61. package/dist/types/ui/UIPopover.d.ts +0 -2
  62. package/dist/types/ui/UIScreen.d.ts +12 -17
  63. package/dist/types/ui/UIScrollable.d.ts +1 -4
  64. package/dist/types/ui/UIText.d.ts +0 -2
  65. package/dist/types/ui/UIVideo.d.ts +3 -5
  66. package/dist/types/ui/UIVirtualizedList.d.ts +14 -16
  67. package/dist/types/ui/UIWidget.d.ts +10 -9
  68. package/dist/types/ui/colorKeys.gen.d.ts +9 -0
  69. package/dist/types/ui/presentable.d.ts +46 -32
  70. package/dist/types/ui/router.d.ts +18 -7
  71. package/dist/types/ui/styleColor.d.ts +1 -0
  72. package/dist/types/ui/transitions.d.ts +18 -0
  73. package/dist/types/ui/tree.d.ts +75 -0
  74. package/dist/types/version.d.ts +10 -0
  75. package/dist/types.json +1 -1
  76. package/package.json +12 -3
  77. package/prompts/2d.md +2 -6
  78. package/prompts/3d.md +1 -5
  79. package/prompts/README.md +1 -1
  80. package/prompts/canvas.md +9 -8
  81. package/prompts/compose.ts +1 -1
  82. package/prompts/core.md +3 -3
  83. package/prompts/design.md +1 -1
  84. package/prompts/dist/2d-game.md +473 -239
  85. package/prompts/dist/3d-app.md +553 -205
  86. package/prompts/dist/ar-app.md +435 -202
  87. package/prompts/dist/design.md +113 -95
  88. package/prompts/dist/ui-app.md +386 -170
  89. package/prompts/ui-design.md +2 -3
  90. package/prompts/ui.md +45 -37
  91. package/src/animate/tween/Animation.ts +34 -150
  92. package/src/animate/tween/Timeline.ts +175 -175
  93. package/src/animate/tween/animateValue.ts +6 -3
  94. package/src/animate/tween/easing.ts +10 -3
  95. package/src/animate/tween/spec.ts +41 -15
  96. package/src/audio/Sound.ts +3 -3
  97. package/src/audio/audio.ts +2 -1
  98. package/src/bridges/2d.d.ts +317 -0
  99. package/src/bridges/app.d.ts +91 -0
  100. package/src/bridges/audio.d.ts +97 -0
  101. package/src/bridges/canvas.d.ts +79 -0
  102. package/src/bridges/device.d.ts +72 -0
  103. package/src/bridges/fetch.d.ts +80 -0
  104. package/src/bridges/files.d.ts +70 -0
  105. package/src/bridges/gl.d.ts +1133 -0
  106. package/src/bridges/input.d.ts +72 -0
  107. package/src/bridges/media.d.ts +55 -0
  108. package/src/bridges/nav.d.ts +71 -0
  109. package/src/bridges/net.d.ts +52 -0
  110. package/src/bridges/service.d.ts +52 -0
  111. package/src/bridges/socket.d.ts +31 -0
  112. package/src/bridges/storage.d.ts +33 -0
  113. package/src/bridges/tree.d.ts +301 -0
  114. package/src/bridges/types.d.ts +49 -0
  115. package/src/canvas/Canvas.ts +114 -159
  116. package/src/canvas/gen/cssColor.ts +224 -0
  117. package/src/canvas/gen/recorder.ts +212 -0
  118. package/src/canvas/gen/spec.ts +201 -0
  119. package/src/chisel.ts +193 -0
  120. package/src/compile/assetMacro.ts +1 -1
  121. package/src/compile/bundler.ts +11 -2
  122. package/src/compile/compileProject.ts +43 -4
  123. package/src/compile/fontMacro.ts +3 -4
  124. package/src/compile/header.ts +26 -5
  125. package/src/compile/index.ts +3 -1
  126. package/src/compile/liteMaterial.ts +1 -1
  127. package/src/compile/sceneEditor.ts +11 -26
  128. package/src/core/color.ts +73 -30
  129. package/src/core/pins.ts +51 -0
  130. package/src/core/signals.ts +8 -1
  131. package/src/g2/CharacterController2D.ts +3 -3
  132. package/src/g2/Node2D.ts +57 -39
  133. package/src/g2/Physics2D.ts +2 -2
  134. package/src/g2/Scene2D.ts +35 -23
  135. package/src/g2/Texture2D.ts +1 -1
  136. package/src/g2/loop.ts +4 -4
  137. package/src/gl/CameraPlace.ts +52 -52
  138. package/src/gl/Foliage.ts +72 -17
  139. package/src/gl/Geometry.ts +1 -2
  140. package/src/gl/Light.ts +10 -0
  141. package/src/gl/Lightmap.ts +45 -29
  142. package/src/gl/Material.ts +95 -51
  143. package/src/gl/Mesh.ts +120 -120
  144. package/src/gl/Model.ts +21 -16
  145. package/src/gl/Node.ts +91 -24
  146. package/src/gl/Particles.ts +1 -1
  147. package/src/gl/Scene.ts +100 -44
  148. package/src/gl/Texture.ts +8 -7
  149. package/src/gl/animation/AnimationClip.ts +1 -1
  150. package/src/gl/animation/DynamicBone.ts +482 -482
  151. package/src/gl/animation/Locomotion.ts +8 -3
  152. package/src/gl/nav/NavMesh.ts +3 -4
  153. package/src/gl/physics/Physics.ts +2 -2
  154. package/src/gl/physics/physicsEvents.ts +3 -3
  155. package/src/gl/scenarios.ts +291 -291
  156. package/src/gl/terrain/Terrain.ts +4 -5
  157. package/src/gl/touch.ts +14 -15
  158. package/src/host.d.ts +2 -3
  159. package/src/inject.editor.ts +7 -0
  160. package/src/inject.ts +13 -16
  161. package/src/net/core.ts +6 -5
  162. package/src/net/index.ts +1 -1
  163. package/src/net/replication.ts +1 -1
  164. package/src/plugin.ts +191 -0
  165. package/src/plugins/gen/camera/contract.d.ts +27 -0
  166. package/src/plugins/gen/camera/sdk/camera.gen.ts +46 -0
  167. package/src/plugins/gen/camera/sdk/camera.ts +57 -0
  168. package/src/plugins/gen/geolocation/contract.d.ts +50 -0
  169. package/src/plugins/gen/geolocation/sdk/geolocation.gen.ts +54 -0
  170. package/src/plugins/{geolocation.ts → gen/geolocation/sdk/geolocation.ts} +22 -43
  171. package/src/plugins/gen/map/contract.d.ts +144 -0
  172. package/src/plugins/gen/map/sdk/map.gen.ts +88 -0
  173. package/src/plugins/{map.ts → gen/map/sdk/map.ts} +68 -102
  174. package/src/plugins/gen/push/contract.d.ts +61 -0
  175. package/src/plugins/gen/push/sdk/push.gen.ts +60 -0
  176. package/src/plugins/gen/push/sdk/push.ts +105 -0
  177. package/src/plugins/gen/qr-scanner/contract.d.ts +16 -0
  178. package/src/plugins/gen/qr-scanner/sdk/qr-scanner.gen.ts +29 -0
  179. package/src/plugins/gen/qr-scanner/sdk/qr-scanner.ts +52 -0
  180. package/src/plugins/permission.ts +5 -4
  181. package/src/runtime/app.ts +20 -9
  182. package/src/runtime/appEvents.ts +5 -4
  183. package/src/runtime/channel.ts +18 -15
  184. package/src/runtime/clipboard.ts +4 -3
  185. package/src/runtime/datetime.ts +2 -1
  186. package/src/runtime/device.ts +17 -15
  187. package/src/runtime/fetch.ts +30 -20
  188. package/src/runtime/files.ts +16 -15
  189. package/src/runtime/input.ts +12 -10
  190. package/src/runtime/media.ts +50 -46
  191. package/src/runtime/misc.ts +7 -3
  192. package/src/runtime/net.ts +8 -7
  193. package/src/runtime/rpc.ts +1 -3
  194. package/src/runtime/service.ts +19 -14
  195. package/src/runtime/share.ts +4 -3
  196. package/src/runtime/storage.ts +6 -4
  197. package/src/runtime/touch.ts +32 -0
  198. package/src/scene/defineScene.ts +61 -365
  199. package/src/scene/editor.ts +408 -0
  200. package/src/scene/editorPlugins.ts +3 -3
  201. package/src/scene/gizmos.ts +15 -9
  202. package/src/server/db/marci/query.ts +1 -1
  203. package/src/server/host.ts +1 -1
  204. package/src/server/runtime.ts +1 -1
  205. package/src/ui/NativeView.ts +72 -25
  206. package/src/ui/UI.ts +3 -3
  207. package/src/ui/UIBottomSheet.ts +16 -17
  208. package/src/ui/UIButton.ts +54 -16
  209. package/src/ui/UIContainer.ts +0 -6
  210. package/src/ui/UIImage.ts +34 -30
  211. package/src/ui/UIInput.ts +29 -37
  212. package/src/ui/UIModal.ts +1 -3
  213. package/src/ui/UINode.ts +347 -297
  214. package/src/ui/UIPager.ts +93 -78
  215. package/src/ui/UIPopover.ts +0 -2
  216. package/src/ui/UIScreen.ts +41 -36
  217. package/src/ui/UIScrollable.ts +19 -13
  218. package/src/ui/UISpacer.ts +1 -1
  219. package/src/ui/UITabs.ts +8 -6
  220. package/src/ui/UIText.ts +7 -17
  221. package/src/ui/UIVideo.ts +30 -27
  222. package/src/ui/UIVirtualizedList.ts +58 -59
  223. package/src/ui/UIWidget.ts +38 -18
  224. package/src/ui/colorKeys.gen.ts +37 -0
  225. package/src/ui/fonts.ts +2 -2
  226. package/src/ui/presentable.ts +59 -42
  227. package/src/ui/router.ts +52 -35
  228. package/src/ui/styleColor.ts +56 -0
  229. package/src/ui/theme.ts +7 -6
  230. package/src/ui/transitions.ts +249 -0
  231. package/src/ui/tree.ts +346 -0
  232. package/src/version.ts +24 -0
  233. package/tests/helpers/engineWorld.ts +11 -0
  234. package/tests/helpers/fakeTree.ts +353 -0
  235. package/tests/helpers/hostStubs.ts +31 -0
  236. package/tests/helpers/index.ts +14 -0
  237. package/tests/helpers/memoryMarci.ts +124 -0
  238. package/tests/helpers/phases.ts +23 -0
  239. package/tests/helpers/preload.ts +18 -0
  240. package/tests/helpers/stubApp.ts +2 -0
  241. package/tests/helpers/stubDevice.ts +2 -0
  242. package/tests/helpers/stubFetch.ts +2 -0
  243. package/tests/helpers/stubInput.ts +2 -0
  244. package/dist/inject.js +0 -4629
  245. package/dist/types/core/registry.d.ts +0 -7
  246. package/dist/types/plugins/camera.d.ts +0 -25
  247. package/dist/types/plugins/push.d.ts +0 -46
  248. package/src/bridges.d.ts +0 -1769
  249. package/src/compile/__tests__/assetIconMacro.test.ts +0 -219
  250. package/src/compile/__tests__/assetMacro.test.ts +0 -100
  251. package/src/compile/__tests__/assetName.test.ts +0 -55
  252. package/src/compile/__tests__/compile.test.ts +0 -310
  253. package/src/compile/__tests__/detectEntry.test.ts +0 -151
  254. package/src/compile/__tests__/fontMacro.test.ts +0 -199
  255. package/src/compile/__tests__/serverSplit.test.ts +0 -27
  256. package/src/core/__tests__/stateMachine.test.ts +0 -132
  257. package/src/core/registry.ts +0 -23
  258. package/src/plugins/camera.ts +0 -81
  259. package/src/plugins/push.ts +0 -132
  260. package/src/plugins/qr.ts +0 -73
package/src/ui/UINode.ts CHANGED
@@ -1,13 +1,22 @@
1
1
  import type { FetchResponse } from "../runtime/fetch"
2
+ import type { ColorInput } from "../core/color"
2
3
  import { createBinding, disposeBinding } from "../core/signals"
3
4
  import type { Animation } from "../animate/tween/Animation"
4
5
  import { CLOCK_UI, DOM_UI, KIND_DISCRETE, KIND_TRANSFORM, KIND_TRANSFORM_MATRIX, parseTransformList, uiValue, type TweenChannel, type TweenMeta } from "../animate/tween/spec"
5
6
  import { tweenBag } from "../animate/tween/Timeline"
7
+ import { wireStyle, wireValue } from "./styleColor"
8
+ import { DEAD_HANDLE, TREE_FLAG_LAYOUT, elementsOf, ensureEmitter, pin, tree, unpin, type TreeElement } from "./tree"
9
+
10
+ // The UI element model since phase 9 (docs/plans/hosts-unification-plan.md): the runtime OWNS the tree —
11
+ // every element is a HANDLE over a runtime node that exists from the constructor on (`_h`: the
12
+ // owned handle, `_h.id` the node id, its collection frees a detached node). Structure, styles and content live in the
13
+ // runtime; the element keeps only what the app set (`_style`, `_text`, …) for reads, its listeners,
14
+ // and its flags. `children` is a snapshot read from the runtime, `parent` is available.
6
15
 
7
16
  export interface UINode {
8
17
  readonly type: string
9
18
  /** Author-given semantic name (a stable selector for tests + AI review feedback). */
10
- readonly name?: string
19
+ name?: string
11
20
  style: Style<this, any>
12
21
  /** Style-class proxy — read/set/toggle/bind the `$`-classes declared in `.style()`. See {@link Classes}. */
13
22
  readonly class: Classes<this>
@@ -21,9 +30,11 @@ export type UINodeChild = UINode | null | undefined | false
21
30
  * needs no spread. */
22
31
  export type UIChildArg = UINodeChild | UINodeChild[]
23
32
 
24
- /** A color: CSS-style string (`"#1c1c1e"`, `"rgba(0,0,0,0.5)"`, `"var(--primaryColor)"`) or
25
- * packed number; `null` clears. */
26
- export type Color = number | string | null
33
+ /** A color: any CSS color string (`"#1c1c1e"`, `"rgba(0,0,0,0.5)"`, `"green"`, `"hsl(210 50% 40%)"`),
34
+ * a theme reference (`"var(--primaryColor)"`), an opaque `0xRRGGBB` number or `[r, g, b(, a)]` in
35
+ * 0..1; `null` clears. A string is the core's to parse (an unknown one is transparent, as in
36
+ * CSS); a number or an array that is not a color throws at the write. */
37
+ export type Color = ColorInput | null
27
38
 
28
39
  /** Style props every element accepts. */
29
40
  export type BaseStyle = {
@@ -37,12 +48,6 @@ export type BaseStyle = {
37
48
  bgColor?: Color | null,
38
49
  overflow?: "visible" | "hidden",
39
50
  boxSizing?: "border-box" | "content-box",
40
- /**
41
- * Author-given semantic name — a stable selector for tests and for marking elements in the AI
42
- * code-review feedback loop. NOT a style: it's extracted at construction (never sent to the layout
43
- * engine) and surfaced on the node + in the renderer's serialized output. Available on every element.
44
- */
45
- name?: string,
46
51
  }
47
52
 
48
53
  /** Painted-box props: background, gradient, border, radius. */
@@ -187,7 +192,141 @@ export type TextStyle = {
187
192
  textOverflow?: "ellipsis" | "clip"
188
193
  }
189
194
 
190
- // Pulls function-valued props out of `style` (mutating it — they must never reach the host bridge)
195
+ /** Reactive style input: every primitive-valued prop also accepts a `() => value` binding that
196
+ * re-applies when a signal it read changes. Nested `$class` blocks stay static — bindings are
197
+ * extracted at the top level only. */
198
+ export type Reactive<T> = { [K in keyof T]: NonNullable<T[K]> extends object ? T[K] : T[K] | (() => T[K]) }
199
+
200
+ /** The curve of a `$class` block's swap (with a `duration`): a curve name (`outCubic`,
201
+ * `inOutSine`, `smoothstep` — the default — …), `cubic-bezier(x1,y1,x2,y2)` or `steps(n)`. A
202
+ * function cannot ride a style block; use `animateTo` for those. */
203
+ export type LayerEasing = string
204
+
205
+ /** Style states: any `$`-prefixed key in `.style()` declares a state block; `duration`/`delay`/
206
+ * `easing` animate its swap. User classes (`$checked`, `$selected`, …) are toggled from code via the
207
+ * `el.class` proxy and INHERIT down the tree (CSS-`.dark`-on-body style): a class set on a node also
208
+ * activates same-name `$` blocks on all descendants within the same root (sdk/docs/ui/classes.md).
209
+ *
210
+ * Five classes are reserved — the system toggles them, `el.class` cannot:
211
+ * - `$hovered` / `$pressed` / `$focused` — the host's hover (mouse only) / press / focus. They
212
+ * cascade from the node the host toggled and STOP at the nearest interactive descendant (a
213
+ * button inside a pressed card is not pressed; it has its own scope).
214
+ * - `$landscape` / `$portrait` — the display orientation, GLOBAL: active on every node at once.
215
+ *
216
+ * Precedence on the same prop: base < `$landscape`/`$portrait` < user `$classes` (later-declared
217
+ * beats earlier) < `$hovered` < `$pressed` < `$focused`.
218
+ *
219
+ * Instantiated with the node's FULL style `T` — a class block accepts everything the node's
220
+ * `.style()` does. Narrowing it wrongly rejects valid props such as `$active: { color }`; guarded
221
+ * by tests/ui/types.test.ts. */
222
+ export type ClassStyles<T> = { [key: `$${string}`]: T & { duration?: number, delay?: number, easing?: LayerEasing } }
223
+
224
+ /** Value accepted by a `el.class` write: a boolean sets the class, a `() => boolean` binds it —
225
+ * the class then tracks the signals the function reads (re-evaluated on change). Reads through
226
+ * the proxy always return a plain boolean, never the bound function. */
227
+ export type ClassValue = boolean | (() => boolean)
228
+
229
+ /** The `el.class` proxy — the runtime switch for `$`-class blocks declared in `.style()`
230
+ * (see {@link ClassStyles}). One surface, mirroring `el.style`:
231
+ *
232
+ * ```ts
233
+ * el.class.checked // read: is it active on THIS element? → boolean
234
+ * el.class.checked = true // activate (false deactivates)
235
+ * el.class.open = !el.class.open // toggle
236
+ * el.class.done = () => todo.done.value // reactive binding (signals)
237
+ * el.class({ checked: true, done: () => … }) // batch form — returns the element, chainable
238
+ * ```
239
+ *
240
+ * Class names are accepted with or without the declaration-site `$` prefix. A write of a reserved
241
+ * class (`hovered` `pressed` `focused` `landscape` `portrait`) throws — the system toggles those.
242
+ * `R` is the concrete node type, so the batch form chains like `.style()`.
243
+ *
244
+ * Typing note: property access is `any` because an index signature can't give reads (`boolean`)
245
+ * and writes ({@link ClassValue}) different types — the batch form is the fully typed path;
246
+ * single-key writes are runtime-coerced with `!!`. */
247
+ export type Classes<R> = ((classes: Record<string, ClassValue>) => R) & { [key: string]: any }
248
+
249
+ /** Members every UI element shares — element interfaces extend this so docs and types live in one
250
+ * place. `S` is the element's style object; `A` is the subset `animateTo` accepts (defaults to `S`). */
251
+ export interface UIElementBase<S extends object, A extends object = S> {
252
+ /** Style: `.style({...})` merges (chainable); `el.style.key = v` writes one prop, a `() => v`
253
+ * value binds it to signals — see {@link Style}. */
254
+ style: Style<this, S>
255
+ /** Author-given semantic name — a stable selector for tests and for marking elements in the AI
256
+ * code-review feedback loop; surfaced in the renderer's serialized output. Set it with `.named()`
257
+ * (chainable) or assign it. */
258
+ name?: string
259
+ /** Set the semantic name (chainable). */
260
+ named(name: string): this
261
+ /** Tween to the target style — meta keys `duration`/`delay`/`loop`/…, see {@link AnimateStyle}. */
262
+ animateTo: AnimateStyle<this, A>
263
+ /** Tween from the given style to the current one (entrance animations). */
264
+ animateFrom: AnimateStyle<this, A>
265
+ /** Fires after every layout pass with the parent-relative box. */
266
+ onLayout(onLayout: OnLayoutCallback): this
267
+ /** Absolute rect in device space, read live (includes scroll); `null` before layout. */
268
+ getBoundingClientRect(): BoundingClientRect | null
269
+ /** The parent element, or `null` for a root (a screen, a widget, an unmounted subtree's top). */
270
+ readonly parent: UINode | null
271
+ /** Free this element's subtree now (views, layout, native state). Only for a DETACHED subtree —
272
+ * remove / hide / close it first; an attached one is left alone. After it the element is dead. */
273
+ destroy(): void
274
+ /** Style-class proxy — read `el.class.checked`, set `el.class.checked = true`, toggle with
275
+ * `!el.class.checked`, bind `el.class.done = () => sig.value`, batch/chain `el.class({ … })`.
276
+ * Classes cascade to descendants. See {@link Classes}. */
277
+ readonly class: Classes<this>
278
+ }
279
+
280
+ /** {@link UIElementBase} plus the child-management surface every container shares. */
281
+ export interface UIContainerBase<S extends object, A extends object = S> extends UIElementBase<S, A> {
282
+ /** Append children. */
283
+ append(...nodes: UINodeChild[]): this
284
+ /** Insert children at `index`. */
285
+ insert(index: number, ...nodes: UINodeChild[]): this
286
+ /** Remove (unmount) the given children. */
287
+ remove(...nodes: UINodeChild[]): this
288
+ /** Replace all children — an array, or a function for reactive children. */
289
+ setContent(nodes: UINodeChild[] | ChildrenFn): this
290
+ /** The children — a SNAPSHOT read from the runtime; mutate via `append`/`insert`/`remove`/`setContent`. */
291
+ readonly children: UINode[]
292
+ }
293
+
294
+ // NB: do NOT annotate `this: R` on StyleFn. As a parameter it makes R contravariant, turning every
295
+ // node type INVARIANT (UIScreen<false> stopped being assignable to UIScreen<boolean>). Runtime
296
+ // `this` is already fixed by `styleFunction.bind(this)`; R stays covariant in the return, so
297
+ // `.style()` still chains. Guarded by tests/ui/types.test.ts.
298
+ export type StyleFn <R, T extends object> = ((style: Reactive<T> & ClassStyles<T>) => R)
299
+
300
+ /** The `el.style` surface: callable — `.style({...})` merges and returns the element for
301
+ * chaining — and per-key readable/writable (`el.style.opacity = 0.5`; a `() => value` write
302
+ * installs a reactive binding). Reads answer what the app set. */
303
+ export type Style <R, T extends object> = StyleFn<R,T> & T & ClassStyles<T>
304
+
305
+ /** Style props of an animate bag: a single value (tween from the current value) or an array of
306
+ * KEYFRAMES (`opacity: [1, 0.3, 1]`, offsets via `times`). */
307
+ export type AnimateProps<T extends object> = { [K in keyof T]?: T[K] | T[K][] }
308
+
309
+ /** The `animateTo`/`animateFrom` surface: target style props (single values or keyframe arrays) plus
310
+ * the flat meta keys — `duration` / `delay` / `easing` / `times` / `loop` / `loopMode` / `commit` /
311
+ * `clock`, see {@link TweenMeta}. Returns the {@link Animation} handle (seek, rate, `finished`). */
312
+ export type AnimateStyle<R, T extends object> = ((style: AnimateProps<T> & TweenMeta) => Animation)
313
+
314
+
315
+ export type AppearStyle <R, T> = (arg: { from: T, duration?: number, delay?: number }) => R
316
+ export type DisappearStyle <R, T> = (arg: { to: T, duration?: number, delay?: number }) => R
317
+
318
+ /** `onLayout` payload — the node's box in PARENT-relative coordinates. */
319
+ export type OnLayoutCallback = (layout: { left: number, top: number, width: number, height: number }) => void
320
+
321
+ /** Web-DOMRect shape on purpose (getBoundingClientRect priors must hold): all eight fields, with
322
+ * right/bottom/x/y derived SDK-side — hosts only report [left, top, width, height]. */
323
+ export type BoundingClientRect = {
324
+ x: number, y: number,
325
+ left: number, top: number, right: number, bottom: number,
326
+ width: number, height: number,
327
+ }
328
+
329
+ // Pulls function-valued props out of `style` (mutating it — they must never reach the bridge)
191
330
  // and returns them. A static value under the same key overrides (disposes) an existing binding.
192
331
  function extractBindings(el: any, style: any): [string, () => any][] | null {
193
332
  let fns: [string, () => any][] | null = null
@@ -204,8 +343,7 @@ function extractBindings(el: any, style: any): [string, () => any][] | null {
204
343
  }
205
344
 
206
345
  // One reactive binding per style key: re-runs when a signal the function read changes, writing
207
- // through the style proxy (so a mounted node hits _creatorUI.updateStyle, an unmounted one just
208
- // keeps _style current for the next mount).
346
+ // through the style proxy.
209
347
  function applyBindings(el: any, fns: [string, () => any][]) {
210
348
  for (const [ key, fn ] of fns) {
211
349
  createBinding(el, key, () => { el.style[key] = fn() })
@@ -214,20 +352,12 @@ function applyBindings(el: any, fns: [string, () => any][]) {
214
352
 
215
353
  // NB: returns `this` — `.style()` must chain. Don't rewrite.
216
354
  function styleFunction(this: any, newStyle: any) {
217
- // `name` is a JS-node selector (read by hosts off el.name), not a layout property — extract it
218
- // exactly like the constructor so `.style({ name })` is equivalent to the first-arg style. Left
219
- // in the style it would never become el.name (breaking name-based lookups) and would leak into
220
- // the layout engine.
221
- if (newStyle && typeof newStyle.name === "string" && newStyle.name.length > 0) {
222
- this.name = newStyle.name
223
- delete newStyle.name
224
- }
225
- if (newStyle) this._mapStyle?.(newStyle)
355
+ if (!newStyle) return this
356
+ this._mapStyle?.(newStyle)
226
357
  const fns = extractBindings(this, newStyle)
358
+ const wire = wireStyle(newStyle) // before the store: a bad color throws with `_style` untouched
227
359
  Object.assign(this._style, newStyle)
228
- if (this._id !== 0) {
229
- _creatorUI.mergeStyle(this._id, newStyle)
230
- }
360
+ if (this._h.id !== 0) tree().mergeStyle(this._h.id, wire)
231
361
  if (fns) applyBindings(this, fns)
232
362
  return this
233
363
  }
@@ -254,9 +384,8 @@ const handler: ProxyHandler<any> = {
254
384
  el._mapStyle(mapped)
255
385
  value = mapped[key]
256
386
  }
257
- if (el._id !== 0) {
258
- _creatorUI.updateStyle(el._id, key, value)
259
- }
387
+ const wire = wireValue(key, value)
388
+ if (el._h.id !== 0) tree().setStyle(el._h.id, key, wire as any)
260
389
  el._style[key] = value
261
390
  return true
262
391
  },
@@ -273,6 +402,20 @@ const handler: ProxyHandler<any> = {
273
402
  // `$checked` and `checked` name the same class — `$` is only the declaration-site prefix in .style().
274
403
  const classKey = (name: string) => name[0] === "$" ? name.slice(1) : name
275
404
 
405
+ // The classes the system toggles (ClassStyles): the host's hover / press / focus, the engine's
406
+ // orientation. `el.class` reads them like any class but never writes them.
407
+ const RESERVED_CLASSES = new Set(["hovered", "pressed", "focused", "landscape", "portrait"])
408
+
409
+ // The class a write names, without the `$`; a reserved one throws before anything is written.
410
+ function writableClass(name: string): string {
411
+ const key = classKey(name)
412
+ if (RESERVED_CLASSES.has(key)) {
413
+ const query = key === "pressed" ? "; read `button.isPressed`" : key === "hovered" ? "; read `button.isHovered`" : ""
414
+ throw new Error(`el.class: \`$${key}\` is a reserved class the system toggles, not a class to set${query}`)
415
+ }
416
+ return key
417
+ }
418
+
276
419
  // Single-class state write, shared by the set trap, the batch form and binding re-runs. No-op when
277
420
  // the state already matches, so an unchanged binding re-run doesn't resend over the bridge.
278
421
  function writeClass(el: any, name: string, enabled: boolean) {
@@ -282,16 +425,14 @@ function writeClass(el: any, name: string, enabled: boolean) {
282
425
  } else {
283
426
  el._classes.delete(name)
284
427
  }
285
- if (el._id !== 0) {
286
- _creatorUI.setClass(el._id, name, enabled)
287
- }
428
+ if (el._h.id !== 0) tree().setClass(el._h.id, name, enabled)
288
429
  }
289
430
 
290
431
  // One class write: a function value installs/replaces a reactive binding, a plain value replaces
291
432
  // the binding (last write wins — unlike the style proxy's hot-path caveat, a stale class binding
292
433
  // silently reverting a later plain write would be a footgun, and class flips are never per-frame).
293
434
  function writeClassValue(el: any, name: string, value: ClassValue) {
294
- name = classKey(name)
435
+ name = writableClass(name)
295
436
  if (typeof value === "function") {
296
437
  createBinding(el, "#class:" + name, () => { writeClass(el, name, !!value()) })
297
438
  } else {
@@ -302,7 +443,9 @@ function writeClassValue(el: any, name: string, value: ClassValue) {
302
443
 
303
444
  // The callable target of the el.class proxy (batch form), bound to the element like styleFunction.
304
445
  function classFunction(this: any, classes: Record<string, ClassValue>) {
305
- for (const key of Object.keys(classes)) {
446
+ const keys = Object.keys(classes)
447
+ for (const key of keys) writableClass(key) // a reserved name rejects the whole batch
448
+ for (const key of keys) {
306
449
  writeClassValue(this, key, classes[key])
307
450
  }
308
451
  return this
@@ -328,179 +471,67 @@ const classHandler: ProxyHandler<any> = {
328
471
  }
329
472
  }
330
473
 
331
- /** Reactive style input: every primitive-valued prop also accepts a `() => value` binding that
332
- * re-applies when a signal it read changes. Nested state blocks (onPressed, onLandscape, …) stay
333
- * static — bindings are extracted at the top level only. */
334
- export type Reactive<T> = { [K in keyof T]: NonNullable<T[K]> extends object ? T[K] : T[K] | (() => T[K]) }
335
-
336
- /** The curve of a state block's swap (`$class` / `onPressed` / `onFocused` with a `duration`): a
337
- * curve name (`outCubic`, `inOutSine`, `smoothstep` — the default — …), `cubic-bezier(x1,y1,x2,y2)`
338
- * or `steps(n)`. A function cannot ride a style block; use `animateTo` for those. */
339
- export type LayerEasing = string
340
-
341
- /** User-defined style classes: any `$`-prefixed key in `.style()` declares a state block toggled
342
- * from code via the `el.class` proxy; `duration`/`delay`/`easing` animate the swap. Class state INHERITS
343
- * down the tree (CSS-`.dark`-on-body style): a class set on a node also activates same-name `$`
344
- * blocks on all descendants within the same root (docs/style-class-cascade-plan.md).
345
- * `$pressed`/`$focused` are reserved — hosts toggle them on press/focus (they beat other
346
- * classes, lose to onPressed/onFocused), so children can react to an ancestor's press.
347
- *
348
- * Instantiated with the node's FULL style `T` — a class block accepts everything the node's
349
- * `.style()` does, like `onLandscape`. Narrowing it wrongly rejects valid props such as
350
- * `$active: { color }`; guarded by tests/ui-types.test.ts. */
351
- export type ClassStyles<T> = { [key: `$${string}`]: T & { duration?: number, delay?: number, easing?: LayerEasing } }
352
-
353
- /** Value accepted by a `el.class` write: a boolean sets the class, a `() => boolean` binds it —
354
- * the class then tracks the signals the function reads (re-evaluated on change). Reads through
355
- * the proxy always return a plain boolean, never the bound function. */
356
- export type ClassValue = boolean | (() => boolean)
357
-
358
- /** The `el.class` proxy — the runtime switch for `$`-class blocks declared in `.style()`
359
- * (see {@link ClassStyles}). One surface, mirroring `el.style`:
360
- *
361
- * ```ts
362
- * el.class.checked // read: is it active on THIS element? → boolean
363
- * el.class.checked = true // activate (false deactivates)
364
- * el.class.open = !el.class.open // toggle
365
- * el.class.done = () => todo.done.value // reactive binding (signals)
366
- * el.class({ checked: true, done: () => … }) // batch form — returns the element, chainable
367
- * ```
368
- *
369
- * Class names are accepted with or without the declaration-site `$` prefix. `R` is the concrete
370
- * node type, so the batch form chains like `.style()`.
371
- *
372
- * Typing note: property access is `any` because an index signature can't give reads (`boolean`)
373
- * and writes ({@link ClassValue}) different types — the batch form is the fully typed path;
374
- * single-key writes are runtime-coerced with `!!`. */
375
- export type Classes<R> = ((classes: Record<string, ClassValue>) => R) & { [key: string]: any }
376
-
377
- /** Members every UI element shares — element interfaces extend this so docs and types live in one
378
- * place. `S` is the element's style object; `A` is the subset `animateTo` accepts (defaults to `S`). */
379
- export interface UIElementBase<S extends object, A extends object = S> {
380
- // NB: no `name` member here. Declaring it would give every element a property in common with
381
- // the style types (BaseStyle.name), letting TS's weak-type check match a NODE against a legacy
382
- // style-first overload — `UIPager(UIText("x"))` must stay a type error (tests/ui-types.test.ts).
383
- /** Style: `.style({...})` merges (chainable); `el.style.key = v` writes one prop, a `() => v`
384
- * value binds it to signals — see {@link Style}. */
385
- style: Style<this, S>
386
- /** Tween to the target style — meta keys `duration`/`delay`/`loop`/…, see {@link AnimateStyle}. */
387
- animateTo: AnimateStyle<this, A>
388
- /** Tween from the given style to the current one (entrance animations). */
389
- animateFrom: AnimateStyle<this, A>
390
- /** Fires after every layout pass with the parent-relative box. */
391
- onLayout(onLayout: OnLayoutCallback): this
392
- /** Absolute rect in device space, read live (includes scroll); `null` before mount. */
393
- getBoundingClientRect(): BoundingClientRect | null
394
- /** Style-class proxy — read `el.class.checked`, set `el.class.checked = true`, toggle with
395
- * `!el.class.checked`, bind `el.class.done = () => sig.value`, batch/chain `el.class({ … })`.
396
- * Classes cascade to descendants. See {@link Classes}. */
397
- readonly class: Classes<this>
398
- }
399
-
400
- /** {@link UIElementBase} plus the child-management surface every container shares. */
401
- export interface UIContainerBase<S extends object, A extends object = S> extends UIElementBase<S, A> {
402
- /** Append children. */
403
- append(...nodes: UINodeChild[]): this
404
- /** Insert children at `index`. */
405
- insert(index: number, ...nodes: UINodeChild[]): this
406
- /** Remove (unmount) the given children. */
407
- remove(...nodes: UINodeChild[]): this
408
- /** Replace all children — an array, or a function for reactive children. */
409
- setContent(nodes: UINodeChild[] | ChildrenFn): this
410
- /** The children array — mutate via `append`/`insert`/`remove`/`setContent`. */
411
- readonly children: UINodeChild[]
412
- }
413
-
414
- /** Orientation state blocks — override props applied only in landscape / portrait (like a
415
- * `$`-class, keyed by device orientation instead of `el.class`). Same full-`T` rule as class
416
- * blocks. */
417
- export type OrientationStyles<T> = {
418
- onLandscape?: T,
419
- onPortrait?: T,
420
- /** @deprecated Misspelling of `onPortrait` (no second "r"). Kept so existing projects and published
421
- * bundles keep working — hosts still honour it — but new code should use `onPortrait`. */
422
- onPortait?: T,
423
- }
424
-
425
- // NB: do NOT annotate `this: R` on StyleFn. As a parameter it makes R contravariant, turning every
426
- // node type INVARIANT (UIScreen<false> stopped being assignable to UIScreen<boolean>). Runtime
427
- // `this` is already fixed by `styleFunction.bind(this)`; R stays covariant in the return, so
428
- // `.style()` still chains. Guarded by tests/ui-types.test-d.ts.
429
- export type StyleFn <R, T extends object> = ((style: Reactive<T> & OrientationStyles<T> & ClassStyles<T>) => R)
430
-
431
- /** The `el.style` surface: callable — `.style({...})` merges and returns the element for
432
- * chaining — and per-key readable/writable (`el.style.opacity = 0.5`; a `() => value` write
433
- * installs a reactive binding). */
434
- export type Style <R, T extends object> = StyleFn<R,T> & T & OrientationStyles<T> & ClassStyles<T>
435
-
436
- /** Style props of an animate bag: a single value (tween from the current value) or an array of
437
- * KEYFRAMES (`opacity: [1, 0.3, 1]`, offsets via `times`). */
438
- export type AnimateProps<T extends object> = { [K in keyof T]?: T[K] | T[K][] }
439
-
440
- /** The `animateTo`/`animateFrom` surface: target style props (single values or keyframe arrays) plus
441
- * the flat meta keys — `duration` / `delay` / `easing` / `times` / `loop` / `loopMode` / `commit` /
442
- * `clock`, see {@link TweenMeta}. Returns the {@link Animation} handle (seek, rate, `finished`). */
443
- export type AnimateStyle<R, T extends object> = ((style: AnimateProps<T> & TweenMeta) => Animation)
444
-
445
-
446
- export type AppearStyle <R, T> = (arg: { from: T, duration?: number, delay?: number }) => R
447
- export type DisappearStyle <R, T> = (arg: { to: T, duration?: number, delay?: number }) => R
448
-
449
- /** `onLayout` payload — the node's box in PARENT-relative coordinates. */
450
- export type OnLayoutCallback = (layout: { left: number, top: number, width: number, height: number }) => void
451
-
452
- /** Web-DOMRect shape on purpose (getBoundingClientRect priors must hold): all eight fields, with
453
- * right/bottom/x/y derived SDK-side — hosts only report [left, top, width, height]. */
454
- export type BoundingClientRect = {
455
- x: number, y: number,
456
- left: number, top: number, right: number, bottom: number,
457
- width: number, height: number,
458
- }
474
+ // ---- the element -----------------------------------------------------------------------------------
459
475
 
460
- export class Element<T extends string> {
476
+ export class Element<T extends string> implements TreeElement {
461
477
  readonly type: T
462
- /** Author-given semantic name (LeCodes `name`) — see BaseStyle.name. Extracted from the options
463
- * object so it never reaches the layout engine; the renderer reads it off the node. */
464
- readonly name?: string
478
+ /** @internal The owned handle: `_h.id` is the node id (the element's identity everywhere; 0 once
479
+ * the node is gone — DEAD_HANDLE), its collection frees a detached subtree. */
480
+ _h: Handle
481
+ /** @internal What the app set — reads answer this; the runtime holds the truth. */
465
482
  protected _style: any
466
-
467
- protected _id: number = 0
483
+ /** @internal TREE_FLAG_* the runtime and the host know about this element. */
484
+ _flags = 0
485
+ private _name?: string
468
486
 
469
487
  protected _styleProxy: any
470
- protected _appear: any
471
- protected _disappear: any
472
488
  /** @internal Reactive bindings owned by this element (style key / "#text" → effect). The element
473
489
  * is the effect's only strong owner, so discarding a subtree lets its bindings collect with it. */
474
490
  _fx?: Map<string, any>
475
491
 
476
492
  /** @internal Per-subclass style normalization (SDK-name → wire-name, e.g. UIInput's
477
493
  * `type: "phone"` → `"tel"`), mutating the style in place before it is stored or sent. Runs on
478
- * every entry point: the first-arg style, `.style()`, and single-key `el.style.k =` writes. */
494
+ * every entry point: the constructor's style, `.style()`, and single-key `el.style.k =` writes. */
479
495
  _mapStyle?(style: any): void
480
496
 
481
497
  // Per-type default styles are inlined at each component's construction site (spread UNDER the
482
498
  // user's style — e.g. UIRow's `flexDirection: "row"`), so every node reaches the host with
483
499
  // explicit values and hosts keep no per-type style opinions of their own.
484
500
  constructor(type: T, style: any) {
501
+ ensureEmitter()
485
502
  this.type = type
486
- if (style && typeof style.name === "string" && style.name.length > 0) {
487
- this.name = style.name
488
- delete style.name
503
+ this._h = tree().create(type)
504
+ // Creation-site capture for an element inspector: the host sets the flag before running the
505
+ // bundle, and the stack (mapped through the bundle's source map) points at the user code that
506
+ // created this element. A host of the tree of truth sees nodes and no element: it sets a
507
+ // FUNCTION, called here — straight after the create, which told the host of the node, so the
508
+ // node the host heard of last is this element's. It is handed the element too: what the app
509
+ // SET (`_style`) is the element's to tell, the runtime keeps what it resolved. Off everywhere
510
+ // else — zero cost when unset.
511
+ const capture = (globalThis as any).__lecodesCaptureSites
512
+ if (capture) {
513
+ const site = (this as any)._site = new Error("lecodes-site")
514
+ if (typeof capture === "function") capture(this._h.id, site, this)
489
515
  }
490
- if (style) this._mapStyle?.(style)
491
- this._style = style
516
+ this._style = style ?? {}
492
517
  if (style) {
518
+ this._mapStyle?.(style)
493
519
  const fns = extractBindings(this, style)
494
- if (fns) applyBindings(this, fns) // sync first run — _style holds concrete values pre-mount
495
- }
496
- // Creation-site capture for the editor preview's element inspector: the host sets the flag
497
- // before running the bundle, and the stack (mapped through the bundle's source map) points at
498
- // the user code that created this element. Off everywhere else — zero cost when unset.
499
- if ((globalThis as any).__lecodesCaptureSites) {
500
- (this as any)._site = new Error("lecodes-site")
520
+ if (Object.keys(style).length > 0) tree().mergeStyle(this._h.id, wireStyle(style))
521
+ if (fns) applyBindings(this, fns)
501
522
  }
502
523
  }
503
524
 
525
+ get name(): string | undefined { return this._name }
526
+ set name(value: string | undefined) {
527
+ this._name = value
528
+ if (this._h.id !== 0) tree().setString(this._h.id, "name", value ?? null)
529
+ }
530
+ named(name: string): this {
531
+ this.name = name
532
+ return this
533
+ }
534
+
504
535
  get style(): any {
505
536
  if (this._styleProxy) return this._styleProxy
506
537
  const obj = styleFunction.bind(this)
@@ -535,7 +566,7 @@ export class Element<T extends string> {
535
566
  if (prop === "text") {
536
567
  if (!("text" in this)) return null
537
568
  return {
538
- domain: DOM_UI, id: () => this._id,
569
+ domain: DOM_UI, id: () => this._h.id,
539
570
  value: (v) => (typeof v === "string" || typeof v === "number" ? { kind: KIND_DISCRETE, str: String(v) } : null),
540
571
  commit: (v) => { (this as any).text = String(v) },
541
572
  }
@@ -544,7 +575,7 @@ export class Element<T extends string> {
544
575
  // A function list tweens per argument when every key shares the list; anything else is handed
545
576
  // to the host as a raw string for matrix decomposition (its old path).
546
577
  return {
547
- domain: DOM_UI, id: () => this._id,
578
+ domain: DOM_UI, id: () => this._h.id,
548
579
  value: (v) => {
549
580
  if (typeof v !== "string") return null
550
581
  const list = parseTransformList(v)
@@ -553,15 +584,15 @@ export class Element<T extends string> {
553
584
  commit: (v) => { this._style.transform = v },
554
585
  }
555
586
  }
587
+ // Keys tween their WIRE value: a color as a string (the runtime makes it a COLOR track), never
588
+ // a FLOAT over a packed number. The commit stores the app's value, like `.style()`.
556
589
  return {
557
- domain: DOM_UI, id: () => this._id, value: uiValue,
590
+ domain: DOM_UI, id: () => this._h.id, value: (v) => uiValue(wireValue(prop, v)),
558
591
  commit: (v) => { this._style[prop] = v },
559
592
  }
560
593
  }
561
594
 
562
- /** @internal Active style-class names (stored without the `$`). Kept on the element — not just in
563
- * the layout engine — so hosts re-apply them when a detached node is re-mounted (screens
564
- * re-assign _id on every open). */
595
+ /** @internal Active style-class names (stored without the `$`) — what the class proxy reads. */
565
596
  protected _classes?: Set<string>
566
597
 
567
598
  protected _classProxy?: any
@@ -579,7 +610,9 @@ export class Element<T extends string> {
579
610
  * Names are accepted with or without the leading `$`. A class set on an element CASCADES: it is
580
611
  * also active on every descendant (their same-name `$` blocks light up), until the subtree's
581
612
  * root — hosted screens/widgets don't inherit. Reads reflect only this element's own classes;
582
- * there is no opt-out below an active ancestor. `Object.keys(el.class)` lists the active names. */
613
+ * there is no opt-out below an active ancestor. `Object.keys(el.class)` lists the active names.
614
+ * The reserved classes (`$hovered` `$pressed` `$focused` `$landscape` `$portrait`) are the
615
+ * system's: writing one throws. */
583
616
  get class(): any {
584
617
  if (this._classProxy) return this._classProxy
585
618
  const obj = classFunction.bind(this)
@@ -591,6 +624,19 @@ export class Element<T extends string> {
591
624
  return proxy
592
625
  }
593
626
 
627
+ /** @internal Add TREE_FLAG_* bits (tells the runtime and the host which events to route here). */
628
+ _addFlags(bits: number): void {
629
+ if ((this._flags & bits) === bits) return
630
+ this._flags |= bits
631
+ if (this._h.id !== 0) tree().setFlags(this._h.id, this._flags)
632
+ }
633
+ /** @internal */
634
+ _removeFlags(bits: number): void {
635
+ if ((this._flags & bits) === 0) return
636
+ this._flags &= ~bits
637
+ if (this._h.id !== 0) tree().setFlags(this._h.id, this._flags)
638
+ }
639
+
594
640
  protected ll?: OnLayoutCallback[]
595
641
  /** Observe layout: fires after every host layout pass with the parent-relative box. */
596
642
  onLayout(onLayout: OnLayoutCallback): this {
@@ -599,21 +645,47 @@ export class Element<T extends string> {
599
645
  } else {
600
646
  this.ll = [ onLayout ]
601
647
  }
648
+ this._addFlags(TREE_FLAG_LAYOUT)
602
649
  return this
603
650
  }
651
+ /** @internal */
652
+ _emitLayout(box: { left: number, top: number, width: number, height: number }): void {
653
+ if (!this.ll) return
654
+ for (const cb of this.ll) cb(box)
655
+ }
604
656
 
605
657
  /** Absolute rect in device space — the space `UIWidget` positions in and touch events report
606
658
  * `clientX`/`clientY` in — including scroll offsets, read live at call time (unlike `onLayout`,
607
659
  * whose coordinates are parent-relative and go stale when an ancestor scrolls). `null` before
608
- * the element is mounted and on hosts without the bridge read. Anchor popovers position-once
660
+ * the element is laid out and on hosts without the read. Anchor popovers position-once
609
661
  * at open; don't poll per frame. */
610
662
  getBoundingClientRect(): BoundingClientRect | null {
611
- if (this._id === 0) return null
612
- const rect = _creatorUI.getBoundingClientRect?.(this._id)
663
+ if (this._h.id === 0) return null
664
+ const rect = tree().getBoundingClientRect(this._h.id)
613
665
  if (!rect) return null
614
666
  const [left, top, width, height] = rect
615
667
  return { x: left, y: top, left, top, right: left + width, bottom: top + height, width, height }
616
668
  }
669
+
670
+ /** The parent element, or `null`. Resolves while the parent is itself attached (mounted under a
671
+ * presented / router-held screen, a shown widget, a pager page …); a loose subtree's root is
672
+ * held only by its handle and is not findable from its children. */
673
+ get parent(): any {
674
+ if (this._h.id === 0) return null
675
+ const id = tree().parent(this._h.id)
676
+ return id !== 0 ? (elementsOf([id])[0] ?? null) : null
677
+ }
678
+
679
+ destroy(): void {
680
+ if (this._h.id === 0) return
681
+ tree().destroy(this._h.id)
682
+ this._h = DEAD_HANDLE // the runtime freed the subtree; drop the handle so its record can go
683
+ }
684
+
685
+ /** @internal The runtime freed the node underneath (an ancestor's free): the element is dead. */
686
+ _freed(): void {
687
+ this._h = DEAD_HANDLE
688
+ }
617
689
  }
618
690
 
619
691
  // ---- reactive children --------------------------------------------------------------------------
@@ -667,38 +739,26 @@ export function __uiMap(list: any, render: (item: any, index: number) => any, sl
667
739
  // ---- factory argument dispatch ------------------------------------------------------------------
668
740
 
669
741
  /** @internal Shared argument dispatch for the UI factories (`UIRow(...)`, `UIButton(...)`, …).
670
- * Children are variadic; an array argument is flattened one level, so both the legacy
671
- * `UIColumn([a, b])` and `UIColumn(header, items.map(row), footer)` work. A lone function
672
- * argument is a reactive {@link ChildrenFn}. A plain non-node object in first position is the
673
- * legacy style bag: it is spread over `defaults`, or — when `defaults` is null — stored as-is
674
- * (the Element constructor mutates it in place, matching the old path). The rest-args array is
675
- * taken over as the children array when nothing needs flattening — callers must hand it off. */
742
+ * Children are variadic; an array argument is flattened one level, so both `UIColumn([a, b])` and
743
+ * `UIColumn(header, items.map(row), footer)` work. A lone function argument is a reactive
744
+ * {@link ChildrenFn}. Styles come from `.style()` only. The rest-args array is taken over as the
745
+ * children array when nothing needs flattening — callers must hand it off. */
676
746
  export function buildUI<R>(args: any[], defaults: any, make: (style: any, children: any[] | ChildrenFn) => R): R {
677
747
  const n = args.length
678
- const a0 = n > 0 ? args[0] : undefined
679
- let style: any
680
- let start = 0
681
- if (a0 !== null && typeof a0 === "object" && !Array.isArray(a0) && !(a0 instanceof Element)) {
682
- style = defaults ? { ...defaults, ...a0 } : a0
683
- start = 1
684
- } else {
685
- style = defaults ? { ...defaults } : {}
686
- }
687
- if (n === start + 1) {
688
- const a = args[start]
748
+ const style = defaults ? { ...defaults } : {}
749
+ if (n === 1) {
750
+ const a = args[0]
689
751
  // lone function → reactive children; lone array → used as the children array directly
690
- // (the legacy zero-copy path — nested arrays inside it are NOT flattened)
691
752
  if (typeof a === "function" || Array.isArray(a)) return make(style, a)
692
753
  }
693
754
  // Single pass: reuse the rest-args array as the children array until the first array argument
694
- // shows up, then copy the prefix once and flatten from there. Falsy entries stay — they are
695
- // compacted at mount like always.
755
+ // shows up, then copy the prefix once and flatten from there. Falsy entries are skipped at mount.
696
756
  let flat: any[] | undefined
697
- for (let i = start; i < n; i++) {
757
+ for (let i = 0; i < n; i++) {
698
758
  const a = args[i]
699
759
  if (flat === undefined) {
700
760
  if (Array.isArray(a)) {
701
- flat = args.slice(start, i)
761
+ flat = args.slice(0, i)
702
762
  for (let j = 0; j < a.length; j++) flat.push(a[j])
703
763
  }
704
764
  } else if (Array.isArray(a)) {
@@ -707,22 +767,43 @@ export function buildUI<R>(args: any[], defaults: any, make: (style: any, childr
707
767
  flat.push(a)
708
768
  }
709
769
  }
710
- if (flat !== undefined) return make(style, flat)
711
- return make(style, start === 0 ? args : args.slice(1))
770
+ return make(style, flat !== undefined ? flat : args)
712
771
  }
713
772
 
714
- export class ContainerElement<T extends string> extends Element<T> {
715
- children: any[] = []
773
+ /** A child slot takes a UI element (falsy = conditional, skipped silently). Anything else is
774
+ * reported and skipped — never inserted: a non-element's `_h.id` (an Animation's, say) is an id
775
+ * from ANOTHER space, and the tree would move whatever node happens to own that number. The hint
776
+ * names the usual cause (both come from code written for the older SDK). */
777
+ const acceptChild = (parent: { type: string }, node: any): boolean => {
778
+ if (!node) return false
779
+ if (node instanceof Element) return true
780
+ const hint =
781
+ typeof node === "function" ? "reactive children are the factory's only argument: UIColumn(() => [ ... ])"
782
+ // by its backing fields: in a bundle chisel drops the accessors nobody reads (`finished`)
783
+ : typeof node.play === "function" && ("finished" in node || "_finished" in node) ? "animateTo() / animateFrom() return the Animation, not the element: call it on the element and put the element in the tree"
784
+ : typeof node === "object" && Object.getPrototypeOf(node) === Object.prototype ? "a style object? Styles go through .style() — the factories take children only"
785
+ : "only UI elements (or falsy values, skipped) can be children"
786
+ const what = typeof node === "object" ? (node.constructor?.name || "object") : typeof node
787
+ console.error(`${parent.type}: a child is not a UI element (${what}) — skipped. ${hint}`)
788
+ return false
789
+ }
716
790
 
791
+ export class ContainerElement<T extends string> extends Element<T> {
717
792
  constructor(type: T, style: any, children: any[] | ChildrenFn) {
718
793
  super(type, style)
719
794
  if (typeof children === "function") {
720
795
  this._bindChildren(children)
721
- } else {
722
- this.children = children
796
+ } else if (children && children.length > 0) {
797
+ this.append(...children)
723
798
  }
724
799
  }
725
800
 
801
+ /** The children — a snapshot read from the runtime (falsy entries were never mounted). */
802
+ get children(): any[] {
803
+ if (this._h.id === 0) return []
804
+ return elementsOf(tree().children(this._h.id))
805
+ }
806
+
726
807
  /** @internal Reactive children: re-runs when a signal read inside `fn` changes and reconciles
727
808
  * the result against the mounted children with minimal insert/remove ops. */
728
809
  _bindChildren(fn: ChildrenFn) {
@@ -744,100 +825,71 @@ export class ContainerElement<T extends string> extends Element<T> {
744
825
  * A moved node is remove+insert — it remounts. Falsy entries are compacted away. */
745
826
  _reconcile(next: UINodeChild[]) {
746
827
  const target: any[] = []
747
- for (const c of next) if (c) target.push(c)
748
-
749
- if (this._id === 0) {
750
- this.children.length = 0
751
- this.children.push(...target)
752
- return
753
- }
754
-
755
- // compact holes left by a previous static children array (falsy entries were never mounted)
756
- for (let i = this.children.length - 1; i >= 0; i--) {
757
- if (!this.children[i]) this.children.splice(i, 1)
758
- }
828
+ for (const c of next) if (acceptChild(this, c)) target.push(c)
829
+ if (this._h.id === 0) return
759
830
 
831
+ const current: any[] = this.children
760
832
  const targetSet = new Set(target)
761
- const departed = this.children.filter(c => !targetSet.has(c))
833
+ const departed = current.filter(c => !targetSet.has(c))
762
834
  if (departed.length > 0) this.remove(...departed)
835
+ const cur: any[] = departed.length > 0 ? this.children : current
763
836
 
764
- // this.children now mirrors the mounted order; walk the target and fix mismatches in place
837
+ // `cur` mirrors the mounted order; walk the target and fix mismatches in place
765
838
  for (let i = 0; i < target.length; i++) {
766
- if (this.children[i] === target[i]) continue
839
+ if (cur[i] === target[i]) continue
767
840
  const node = target[i]
768
- if (this.children.includes(node, i + 1)) {
841
+ const j = cur.indexOf(node, i + 1)
842
+ if (j >= 0) {
769
843
  this.remove(node) // moved forward — remount at its new position
844
+ cur.splice(j, 1)
770
845
  }
771
- this.insert(i, node)
846
+ this._insertAt(i, node)
847
+ cur.splice(i, 0, node)
772
848
  }
773
849
  }
774
850
 
775
- /** Append children (falsy entries are kept in the array but never mounted). */
851
+ private _insertAt(index: number, node: any): void {
852
+ if (this._h.id === 0 || !node || node._h.id === 0) return
853
+ tree().insert(this._h.id, node._h.id, index)
854
+ pin(node)
855
+ }
856
+
857
+ /** Append children (falsy entries are skipped). */
776
858
  append(...nodes: UINodeChild[]): this {
777
- if (this._id === 0) {
778
- this.children.push(...nodes)
779
- return this
780
- }
781
- let index = 0
782
- for (let child of this.children) {
783
- if (child) {
784
- index++
785
- }
786
- }
787
- for (let node of nodes) {
788
- if (!node) continue
789
- _creatorUI.insertNode(index, this._id, node)
859
+ if (this._h.id === 0) return this
860
+ let index = tree().children(this._h.id).length
861
+ for (const node of nodes) {
862
+ if (!acceptChild(this, node)) continue
863
+ this._insertAt(index, node)
790
864
  index++
791
865
  }
792
- this.children.push(...nodes)
793
866
  return this
794
867
  }
795
- /** Insert children at `index` of the children array. */
868
+ /** Insert children at `index`. */
796
869
  insert(index: number, ...nodes: UINodeChild[]): this {
797
- if (this._id === 0) {
798
- this.children.splice(index, 0, ...nodes)
799
- return this
800
- }
801
-
802
- let _index = 0
803
- for (let i = 0; i < index; i++) {
804
- if (this.children[i]) {
805
- _index++
806
- }
807
- }
808
- for (let node of nodes) {
809
- if (!node) continue
810
- _creatorUI.insertNode(_index, this._id, node)
811
- _index++
870
+ if (this._h.id === 0) return this
871
+ let at = index
872
+ for (const node of nodes) {
873
+ if (!acceptChild(this, node)) continue
874
+ this._insertAt(at, node)
875
+ at++
812
876
  }
813
- this.children.splice(index, 0, ...nodes)
814
877
  return this
815
878
  }
816
- /** Remove (unmount) the given children. */
817
- remove(...nodesToDelete: UINode[]): this {
818
- const set = new Set(nodesToDelete)
819
-
820
- for (let i = 0; i < this.children.length; i++) {
821
- if (set.has(this.children[i])) {
822
- set.delete(this.children[i])
823
- this.children.splice(i, 1)
824
- i--
825
-
826
- if (set.size === 0) {
827
- break
828
- }
829
- }
830
- }
831
-
832
- if (this._id === 0) {
833
- return this
834
- }
835
- for (let node of nodesToDelete) {
836
- _creatorUI.removeNode(this._id, node)
879
+ /** Remove (unmount) the given children. The removed subtrees stay valid (re-appendable) as long
880
+ * as you hold them; dropped ones are freed. */
881
+ remove(...nodesToDelete: UINodeChild[]): this {
882
+ if (this._h.id === 0) return this
883
+ for (const node of nodesToDelete) {
884
+ if (!(node instanceof Element)) continue // falsy, or never mountable (see acceptChild)
885
+ const el = node as any
886
+ if (el._h.id === 0) continue
887
+ tree().remove(this._h.id, el._h.id)
888
+ unpin(el._h.id)
837
889
  }
838
890
  return this
839
891
  }
840
-
892
+
841
893
  /** Replace all children — a plain array, or a function for reactive children. */
842
894
  setContent(children: UINodeChild[] | ChildrenFn): this {
843
895
  if (typeof children === "function") {
@@ -845,11 +897,9 @@ export class ContainerElement<T extends string> extends Element<T> {
845
897
  return this
846
898
  }
847
899
  disposeBinding(this, "#children") // static content overrides a reactive binding
848
- if (this._id !== 0) {
849
- _creatorUI.setContent(this._id, this.children, children)
850
- }
851
- this.children.length = 0
852
- this.children.push(...children)
900
+ const current = this.children
901
+ if (current.length > 0) this.remove(...current)
902
+ this.append(...children)
853
903
  return this
854
904
  }
855
- }
905
+ }