lecodes-cli 0.18.2 → 0.19.2

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 (224) hide show
  1. package/dist/index.js +1866 -301
  2. package/package.json +14 -12
  3. package/runtime/scene-harness.json +1 -1
  4. package/runtime/sdk-types.json +1 -1
  5. package/src/api.ts +302 -0
  6. package/src/browserAuth.ts +87 -0
  7. package/src/cmgenTool.ts +104 -0
  8. package/src/commands/app.ts +891 -0
  9. package/src/commands/appAndroid.ts +603 -0
  10. package/src/commands/appDesktop.ts +326 -0
  11. package/src/commands/appDesktopMac.ts +470 -0
  12. package/src/commands/appIcon.ts +187 -0
  13. package/src/commands/appShared.ts +448 -0
  14. package/src/commands/appTemplates.ts +883 -0
  15. package/src/commands/appTemplatesAndroid.ts +599 -0
  16. package/src/commands/appTemplatesGradlew.ts +9 -0
  17. package/src/commands/assets.ts +28 -0
  18. package/src/commands/clone.ts +60 -0
  19. package/src/commands/compile.ts +253 -0
  20. package/src/commands/create.ts +59 -0
  21. package/src/commands/design.ts +863 -0
  22. package/src/commands/designTemplates.ts +11 -0
  23. package/src/commands/desktop.ts +122 -0
  24. package/src/commands/dev.ts +214 -0
  25. package/src/commands/diff.ts +59 -0
  26. package/src/commands/init.ts +191 -0
  27. package/src/commands/install.ts +198 -0
  28. package/src/commands/lightmap.ts +290 -0
  29. package/src/commands/link.ts +147 -0
  30. package/src/commands/login.ts +24 -0
  31. package/src/commands/navmesh.ts +225 -0
  32. package/src/commands/pn.ts +305 -0
  33. package/src/commands/projectTemplates.ts +129 -0
  34. package/src/commands/pull.ts +109 -0
  35. package/src/commands/push.ts +173 -0
  36. package/src/commands/render.ts +414 -0
  37. package/src/commands/scene.ts +148 -0
  38. package/src/commands/shaders.ts +157 -0
  39. package/src/commands/status.ts +29 -0
  40. package/src/commands/test.ts +370 -0
  41. package/src/commands/thumbs.ts +178 -0
  42. package/src/commands/types.ts +87 -0
  43. package/src/commands/update.ts +190 -0
  44. package/src/compile/assetIcons.ts +214 -0
  45. package/src/compile/collect.ts +46 -0
  46. package/src/compile/collectLocal.ts +37 -0
  47. package/src/compile/designCompile.ts +104 -0
  48. package/src/compile/fonts.ts +156 -0
  49. package/src/compile/headlessBundle.ts +129 -0
  50. package/src/compile/nativeStack.ts +21 -0
  51. package/src/compile/projectCompile.ts +109 -0
  52. package/src/compile/sceneCompile.ts +113 -0
  53. package/src/compile/screenEntry.ts +127 -0
  54. package/src/compile/shaders.ts +245 -0
  55. package/src/config.ts +42 -0
  56. package/src/designMeta.ts +35 -0
  57. package/src/desktopRenderer.ts +532 -0
  58. package/src/desktopScript.ts +276 -0
  59. package/src/dev/clientTemplates.ts +160 -0
  60. package/src/dev/devServer.ts +292 -0
  61. package/src/dev/wsServer.ts +144 -0
  62. package/src/distRoot.ts +20 -0
  63. package/src/ignore.ts +163 -0
  64. package/src/index.ts +491 -0
  65. package/src/lecodes-3d-editor.d.ts +41 -0
  66. package/src/lecodes-assets.d.ts +7 -0
  67. package/src/lecodes-design.d.ts +191 -0
  68. package/src/lecodes-renderer.d.ts +131 -0
  69. package/src/localFiles.ts +144 -0
  70. package/src/manifest.ts +46 -0
  71. package/src/matcTool.ts +137 -0
  72. package/src/peers.ts +40 -0
  73. package/src/project.ts +61 -0
  74. package/src/projectEnv.ts +94 -0
  75. package/src/qrcode-terminal.d.ts +9 -0
  76. package/src/releases.ts +125 -0
  77. package/src/serverDiff.ts +78 -0
  78. package/src/textDiff.ts +103 -0
  79. package/src/types.ts +0 -0
  80. package/src/util.ts +146 -0
  81. package/runtime/sdk/animate/animate.ts +0 -238
  82. package/runtime/sdk/animate/bezier.ts +0 -138
  83. package/runtime/sdk/animate/easings.ts +0 -126
  84. package/runtime/sdk/canvas/Canvas.ts +0 -305
  85. package/runtime/sdk/core/Aspect.ts +0 -512
  86. package/runtime/sdk/core/InspectorUI.ts +0 -212
  87. package/runtime/sdk/core/color.ts +0 -66
  88. package/runtime/sdk/core/compWrite.ts +0 -42
  89. package/runtime/sdk/core/events.ts +0 -38
  90. package/runtime/sdk/core/fields.ts +0 -120
  91. package/runtime/sdk/core/registry.ts +0 -23
  92. package/runtime/sdk/core/signals.ts +0 -277
  93. package/runtime/sdk/core/time.ts +0 -81
  94. package/runtime/sdk/g2/Camera2D.ts +0 -40
  95. package/runtime/sdk/g2/CharacterController2D.ts +0 -276
  96. package/runtime/sdk/g2/Node2D.ts +0 -267
  97. package/runtime/sdk/g2/OneWay2D.ts +0 -66
  98. package/runtime/sdk/g2/Physics2D.ts +0 -346
  99. package/runtime/sdk/g2/Scene2D.ts +0 -209
  100. package/runtime/sdk/g2/Shape2D.ts +0 -259
  101. package/runtime/sdk/g2/Sprite.ts +0 -89
  102. package/runtime/sdk/g2/SpriteAnimation.ts +0 -171
  103. package/runtime/sdk/g2/SpriteSheet.ts +0 -166
  104. package/runtime/sdk/g2/Texture2D.ts +0 -47
  105. package/runtime/sdk/g2/Tilemap.ts +0 -41
  106. package/runtime/sdk/g2/Tileset.ts +0 -71
  107. package/runtime/sdk/g2/Trigger2D.ts +0 -77
  108. package/runtime/sdk/g2/autotile.ts +0 -433
  109. package/runtime/sdk/g2/cells.ts +0 -91
  110. package/runtime/sdk/g2/defineScene2d.ts +0 -381
  111. package/runtime/sdk/g2/groups2d.ts +0 -106
  112. package/runtime/sdk/g2/loop.ts +0 -50
  113. package/runtime/sdk/g2/scenarios2d.ts +0 -69
  114. package/runtime/sdk/g2/touch.ts +0 -83
  115. package/runtime/sdk/gl/Camera.ts +0 -160
  116. package/runtime/sdk/gl/CameraPlace.ts +0 -52
  117. package/runtime/sdk/gl/CharacterController.ts +0 -238
  118. package/runtime/sdk/gl/Gearbox.ts +0 -212
  119. package/runtime/sdk/gl/Geometry.ts +0 -279
  120. package/runtime/sdk/gl/IK.ts +0 -193
  121. package/runtime/sdk/gl/InstancedMesh.ts +0 -132
  122. package/runtime/sdk/gl/Light.ts +0 -99
  123. package/runtime/sdk/gl/Lightmap.ts +0 -179
  124. package/runtime/sdk/gl/Material.ts +0 -245
  125. package/runtime/sdk/gl/Mesh.ts +0 -83
  126. package/runtime/sdk/gl/Model.ts +0 -64
  127. package/runtime/sdk/gl/Node.ts +0 -350
  128. package/runtime/sdk/gl/Noise.ts +0 -30
  129. package/runtime/sdk/gl/Particles.ts +0 -676
  130. package/runtime/sdk/gl/Physics.ts +0 -222
  131. package/runtime/sdk/gl/Plane.ts +0 -53
  132. package/runtime/sdk/gl/Ray.ts +0 -16
  133. package/runtime/sdk/gl/Scene.ts +0 -479
  134. package/runtime/sdk/gl/Shape.ts +0 -377
  135. package/runtime/sdk/gl/Texture.ts +0 -46
  136. package/runtime/sdk/gl/Trigger.ts +0 -45
  137. package/runtime/sdk/gl/Vehicle.ts +0 -473
  138. package/runtime/sdk/gl/Wheel.ts +0 -240
  139. package/runtime/sdk/gl/animation/AnimationClip.ts +0 -204
  140. package/runtime/sdk/gl/animation/Animator.ts +0 -87
  141. package/runtime/sdk/gl/animation/Layer.ts +0 -29
  142. package/runtime/sdk/gl/animation/Loop.ts +0 -25
  143. package/runtime/sdk/gl/animation/Playback.ts +0 -43
  144. package/runtime/sdk/gl/animation/core.ts +0 -294
  145. package/runtime/sdk/gl/controls.ts +0 -95
  146. package/runtime/sdk/gl/physicsEvents.ts +0 -20
  147. package/runtime/sdk/gl/scenarios.ts +0 -291
  148. package/runtime/sdk/gl/state.ts +0 -6
  149. package/runtime/sdk/gl/touch.ts +0 -68
  150. package/runtime/sdk/inject.ts +0 -186
  151. package/runtime/sdk/math/Mathf.ts +0 -118
  152. package/runtime/sdk/math/mat4.ts +0 -278
  153. package/runtime/sdk/math/quat.ts +0 -232
  154. package/runtime/sdk/math/vec.ts +0 -255
  155. package/runtime/sdk/plugins/camera.ts +0 -81
  156. package/runtime/sdk/plugins/geolocation.ts +0 -123
  157. package/runtime/sdk/plugins/oauth.ts +0 -61
  158. package/runtime/sdk/plugins/permission.ts +0 -7
  159. package/runtime/sdk/plugins/push.ts +0 -132
  160. package/runtime/sdk/plugins/qr.ts +0 -73
  161. package/runtime/sdk/plugins/service.ts +0 -47
  162. package/runtime/sdk/runtime/app.ts +0 -101
  163. package/runtime/sdk/runtime/appEvents.ts +0 -54
  164. package/runtime/sdk/runtime/channel.ts +0 -61
  165. package/runtime/sdk/runtime/clipboard.ts +0 -20
  166. package/runtime/sdk/runtime/datetime.ts +0 -329
  167. package/runtime/sdk/runtime/device.ts +0 -293
  168. package/runtime/sdk/runtime/fetch.ts +0 -77
  169. package/runtime/sdk/runtime/files.ts +0 -23
  170. package/runtime/sdk/runtime/input.ts +0 -175
  171. package/runtime/sdk/runtime/media.ts +0 -111
  172. package/runtime/sdk/runtime/misc.ts +0 -16
  173. package/runtime/sdk/runtime/net.ts +0 -36
  174. package/runtime/sdk/runtime/rpc.ts +0 -218
  175. package/runtime/sdk/runtime/service.ts +0 -83
  176. package/runtime/sdk/runtime/share.ts +0 -9
  177. package/runtime/sdk/runtime/storage.ts +0 -13
  178. package/runtime/sdk/runtime/touch.ts +0 -76
  179. package/runtime/sdk/scene/defineScene.ts +0 -1227
  180. package/runtime/sdk/scene/editorPlugins.ts +0 -92
  181. package/runtime/sdk/scene/gizmos.ts +0 -148
  182. package/runtime/sdk/scene/grammar.ts +0 -120
  183. package/runtime/sdk/scene/material.ts +0 -188
  184. package/runtime/sdk/server/auth/appConfig.ts +0 -12
  185. package/runtime/sdk/server/auth/global.ts +0 -80
  186. package/runtime/sdk/server/auth/host.ts +0 -318
  187. package/runtime/sdk/server/auth/models.ts +0 -83
  188. package/runtime/sdk/server/auth/types.ts +0 -50
  189. package/runtime/sdk/server/channel.ts +0 -56
  190. package/runtime/sdk/server/context.ts +0 -36
  191. package/runtime/sdk/server/db/defineDb.ts +0 -237
  192. package/runtime/sdk/server/db/fields.ts +0 -132
  193. package/runtime/sdk/server/db/httpTransport.ts +0 -93
  194. package/runtime/sdk/server/db/index.ts +0 -7
  195. package/runtime/sdk/server/db/marci/query.ts +0 -412
  196. package/runtime/sdk/server/db/types.ts +0 -202
  197. package/runtime/sdk/server/errors.ts +0 -12
  198. package/runtime/sdk/server/host.ts +0 -74
  199. package/runtime/sdk/server/inject.ts +0 -13
  200. package/runtime/sdk/server/runtime.ts +0 -133
  201. package/runtime/sdk/server/validate.ts +0 -87
  202. package/runtime/sdk/ui/NativeView.ts +0 -142
  203. package/runtime/sdk/ui/UI.ts +0 -39
  204. package/runtime/sdk/ui/UIBottomSheet.ts +0 -139
  205. package/runtime/sdk/ui/UIButton.ts +0 -101
  206. package/runtime/sdk/ui/UIContainer.ts +0 -60
  207. package/runtime/sdk/ui/UIImage.ts +0 -83
  208. package/runtime/sdk/ui/UIInput.ts +0 -185
  209. package/runtime/sdk/ui/UIModal.ts +0 -139
  210. package/runtime/sdk/ui/UINode.ts +0 -826
  211. package/runtime/sdk/ui/UIPager.ts +0 -362
  212. package/runtime/sdk/ui/UIPopover.ts +0 -100
  213. package/runtime/sdk/ui/UIScreen.ts +0 -123
  214. package/runtime/sdk/ui/UIScrollable.ts +0 -87
  215. package/runtime/sdk/ui/UISpacer.ts +0 -14
  216. package/runtime/sdk/ui/UITabs.ts +0 -236
  217. package/runtime/sdk/ui/UIText.ts +0 -51
  218. package/runtime/sdk/ui/UIVideo.ts +0 -88
  219. package/runtime/sdk/ui/UIVirtualizedList.ts +0 -241
  220. package/runtime/sdk/ui/UIWidget.ts +0 -127
  221. package/runtime/sdk/ui/fonts.ts +0 -13
  222. package/runtime/sdk/ui/presentable.ts +0 -117
  223. package/runtime/sdk/ui/router.ts +0 -132
  224. package/runtime/sdk/ui/theme.ts +0 -84
@@ -1,826 +0,0 @@
1
- import type { FetchResponse } from "../runtime/fetch"
2
- import { createBinding, disposeBinding } from "../core/signals"
3
-
4
- export interface UINode {
5
- readonly type: string
6
- /** Author-given semantic name (a stable selector for tests + AI review feedback). */
7
- readonly name?: string
8
- style: Style<this, any>
9
- /** Style-class proxy — read/set/toggle/bind the `$`-classes declared in `.style()`. See {@link Classes}. */
10
- readonly class: Classes<this>
11
- }
12
-
13
- /** A child slot value: a node, or falsy (conditional rendering — falsy entries are skipped). */
14
- export type UINodeChild = UINode | null | undefined | false
15
-
16
- /** A factory child argument: a node (or falsy for conditionals), or an array of them — arrays are
17
- * flattened one level at the argument position, so `UIColumn(header, items.map(row), footer)`
18
- * needs no spread. */
19
- export type UIChildArg = UINodeChild | UINodeChild[]
20
-
21
- /** A color: CSS-style string (`"#1c1c1e"`, `"rgba(0,0,0,0.5)"`, `"var(--primaryColor)"`) or
22
- * packed number; `null` clears. */
23
- export type Color = number | string | null
24
-
25
- /** Style props every element accepts. */
26
- export type BaseStyle = {
27
- opacity?: number | `${number}`,
28
- transform?: string | null,
29
- backgroundColor?: Color | null,
30
- bgColor?: Color | null,
31
- overflow?: "visible" | "hidden",
32
- boxSizing?: "border-box" | "content-box",
33
- /**
34
- * Author-given semantic name — a stable selector for tests and for marking elements in the AI
35
- * code-review feedback loop. NOT a style: it's extracted at construction (never sent to the layout
36
- * engine) and surfaced on the node + in the renderer's serialized output. Available on every element.
37
- */
38
- name?: string,
39
- }
40
-
41
- /** Painted-box props: background, gradient, border, radius. */
42
- export type DrawableStyle = {
43
- backgroundImage?: FetchResponse | File | string | null,
44
- bgGradient?: string,
45
- backgroundGradient?: string,
46
- bgImage?: FetchResponse | File | string | null,
47
- backgroundSize?: "cover" | "contain" | "tile",
48
- bgSize?: "cover" | "contain" | "tile",
49
- border?: string | number | null,
50
- borderWidth?: number,
51
- borderColor?: Color,
52
-
53
- borderLeft?: string | number | null,
54
- borderLeftWidth?: number,
55
- borderLeftColor?: Color,
56
- borderTop?: string | number | null,
57
- borderTopWidth?: number,
58
- borderTopColor?: Color,
59
- borderRight?: string | number | null,
60
- borderRightWidth?: number,
61
- borderRightColor?: Color,
62
- borderBottom?: string | number | null,
63
- borderBottomWidth?: number,
64
- borderBottomColor?: Color,
65
-
66
- borderRadius?: UIValue | string,
67
- borderTopLeftRadius?: UIValue,
68
- borderTopRightRadius?: UIValue,
69
- borderBottomLeftRadius?: UIValue,
70
- borderBottomRightRadius?: UIValue,
71
-
72
- pointerEvents?: "all" | "none",
73
- }
74
-
75
- /** Flex-layout props of a container (how it lays out its children). */
76
- export type ContainerStyle = {
77
- flexDirection?: "row" | "column",
78
- justifyContent?: "flex-start" | "center" | "flex-end" | "space-between" | "space-evenly",
79
- alignItems?: "flex-start" | "center" | "flex-end" | "stretch",
80
- gap?: UIValue,
81
- flexWrap?: "nowrap" | "wrap" | "wrap-reverse"
82
- }
83
-
84
- /** A dimension: a number (logical px) or a CSS-style unit / function string. */
85
- type UIValue = number |
86
- `${number}px` |
87
- `${number}vw` |
88
- `${number}vh` |
89
- `${number}vmin` |
90
- `${number}vmax` |
91
- `${number}em` |
92
- `calc(${string})` |
93
- `min(${string})` |
94
- `max(${string})` |
95
- `${string}%` |
96
- `clamp(${string})` |
97
- `var(--${string})`
98
-
99
- /** Padding, with `p`/`px`/`pl`… shorthands. `safe-*` insets by the device safe area,
100
- * `comfort-*` by the theme's comfort knobs (max of both). */
101
- export type PaddingStyle = {
102
- paddingLeft?: UIValue | "safe-left" | "comfort-left",
103
- paddingTop?: UIValue | "safe-top" | "comfort-top",
104
- paddingRight?: UIValue | "safe-right" | "comfort-right",
105
- paddingBottom?: UIValue | "safe-bottom" | "comfort-bottom",
106
- padding?: UIValue | "safe-all" | "comfort-all",
107
- paddingHorizontal?: UIValue | "comfort-x",
108
- paddingVertical?: UIValue | "comfort-y",
109
-
110
- pl?: UIValue | "safe-left" | "comfort-left",
111
- pt?: UIValue | "safe-top" | "comfort-top",
112
- pr?: UIValue | "safe-right" | "comfort-right",
113
- pb?: UIValue | "safe-bottom" | "comfort-bottom",
114
- p?: UIValue | "safe-all" | "comfort-all",
115
- px?: UIValue | "comfort-x",
116
- py?: UIValue | "comfort-y",
117
- }
118
-
119
- /** Margin, with `m`/`mx`/`ml`… shorthands; `"auto"` centers/pushes like CSS. */
120
- export type MarginStyle = {
121
- marginLeft?: UIValue | "auto" | "safe-left" | "comfort-left",
122
- marginTop?: UIValue | "auto" | "safe-top" | "comfort-top",
123
- marginRight?: UIValue | "auto" | "safe-right" | "comfort-right",
124
- marginBottom?: UIValue | "auto" | "safe-bottom" | "comfort-bottom",
125
- margin?: UIValue | "auto" | "safe-all" | "comfort-all",
126
- marginHorizontal?: UIValue | "auto" | "comfort-x",
127
- marginVertical?: UIValue | "auto" | "comfort-y",
128
-
129
- ml?: UIValue | "auto" | "safe-left" | "comfort-left",
130
- mt?: UIValue | "auto" | "safe-top" | "comfort-top",
131
- mr?: UIValue | "auto" | "safe-right" | "comfort-right",
132
- mb?: UIValue | "auto" | "safe-bottom" | "comfort-bottom",
133
- m?: UIValue | "auto" | "safe-all" | "comfort-all",
134
- mx?: UIValue | "auto" | "comfort-x",
135
- my?: UIValue | "auto" | "comfort-y",
136
- }
137
-
138
- /** Box position (for `position: "absolute"`) and size constraints. */
139
- export type PositionStyle = {
140
- top?: UIValue | `${number}%` | 'safe-top' | 'comfort-top',
141
- left?: UIValue | `${number}%` | 'safe-left' | 'comfort-left',
142
- bottom?: UIValue | `${number}%` | 'safe-bottom' | 'comfort-bottom',
143
- right?: UIValue | `${number}%` | 'safe-right' | 'comfort-right',
144
- /** Shorthand for all four edges at one value. A longhand (`top`, `left`, …) overrides it. */
145
- inset?: UIValue | `${number}%` | "auto",
146
- width?: UIValue | `${number}%` | "auto",
147
- height?: UIValue | `${number}%` | "auto",
148
- maxWidth?: UIValue | `${number}%` | "auto",
149
- maxHeight?: UIValue | `${number}%` | "auto",
150
- minWidth?: UIValue | `${number}%` | "auto",
151
- minHeight?: UIValue | `${number}%` | "auto",
152
- }
153
-
154
- /** The base style of every element: box + flex-child props. */
155
- export type ElementStyle = PaddingStyle & MarginStyle & BaseStyle & PositionStyle & {
156
- position?: "absolute" | "static" | "relative",
157
- aspectRatio?: number,
158
- flex?: number | string,
159
- flexGrow?: number,
160
- flexShrink?: number,
161
- flexBase?: UIValue | `${number}%` | "auto",
162
- alignSelf?: "flex-start" | "center" | "flex-end" | "stretch",
163
- display?: "none" | "flex"
164
- }
165
-
166
- /** Typography props. */
167
- export type TextStyle = {
168
- textAlign?: "start" | "center" | "end" | "left" | "right"
169
- fontFamily?: string,
170
- fontSize?: UIValue,
171
- lineHeight?: UIValue | "normal",
172
- fontWeight?: number | `${number}` | "normal" | "bold",
173
- fontStyle?: "normal" | "italic",
174
- color?: Color,
175
- textDecoration?: "underline" | "line-through" | "none",
176
- letterSpacing?: UIValue,
177
- /** Cap the text at N lines; the box is measured to the clamped height. 0 / omitted = unbounded. */
178
- lineClamp?: number,
179
- /** How a clamped line ends. Only meaningful together with `lineClamp`. Default "ellipsis". */
180
- textOverflow?: "ellipsis" | "clip"
181
- }
182
-
183
- // Pulls function-valued props out of `style` (mutating it — they must never reach the host bridge)
184
- // and returns them. A static value under the same key overrides (disposes) an existing binding.
185
- function extractBindings(el: any, style: any): [string, () => any][] | null {
186
- let fns: [string, () => any][] | null = null
187
- for (const key of Object.keys(style)) {
188
- const value = style[key]
189
- if (typeof value === "function") {
190
- (fns ??= []).push([key, value])
191
- delete style[key]
192
- } else if (el._fx !== undefined) {
193
- disposeBinding(el, key)
194
- }
195
- }
196
- return fns
197
- }
198
-
199
- // One reactive binding per style key: re-runs when a signal the function read changes, writing
200
- // through the style proxy (so a mounted node hits _creatorUI.updateStyle, an unmounted one just
201
- // keeps _style current for the next mount).
202
- function applyBindings(el: any, fns: [string, () => any][]) {
203
- for (const [ key, fn ] of fns) {
204
- createBinding(el, key, () => { el.style[key] = fn() })
205
- }
206
- }
207
-
208
- // NB: returns `this` — `.style()` must chain. Don't rewrite.
209
- function styleFunction(this: any, newStyle: any) {
210
- // `name` is a JS-node selector (read by hosts off el.name), not a layout property — extract it
211
- // exactly like the constructor so `.style({ name })` is equivalent to the first-arg style. Left
212
- // in the style it would never become el.name (breaking name-based lookups) and would leak into
213
- // the layout engine.
214
- if (newStyle && typeof newStyle.name === "string" && newStyle.name.length > 0) {
215
- this.name = newStyle.name
216
- delete newStyle.name
217
- }
218
- if (newStyle) this._mapStyle?.(newStyle)
219
- const fns = extractBindings(this, newStyle)
220
- Object.assign(this._style, newStyle)
221
- if (this._id !== 0) {
222
- _creatorUI.mergeStyle(this._id, newStyle)
223
- }
224
- if (fns) applyBindings(this, fns)
225
- return this
226
- }
227
-
228
- const handler: ProxyHandler<any> = {
229
- get(target, key, receiver) {
230
- if (key === "_raw") {
231
- return target._el._style
232
- }
233
- return target._el._style[key]
234
- },
235
- set(target, key: string, value: string | number) {
236
- if (typeof value === "function") {
237
- // Reactive binding: `el.style.prop = () => sig.value`. Note a later plain write does NOT
238
- // dispose the binding (this is the hot path) — override statically via `.style({ prop })`.
239
- const el = target._el
240
- const fn = value as unknown as () => any
241
- createBinding(el, key, () => { el.style[key] = fn() })
242
- return true
243
- }
244
- const el = target._el
245
- if (el._mapStyle !== undefined) {
246
- const mapped = { [key]: value }
247
- el._mapStyle(mapped)
248
- value = mapped[key]
249
- }
250
- if (el._id !== 0) {
251
- _creatorUI.updateStyle(el._id, key, value)
252
- }
253
- el._style[key] = value
254
- return true
255
- },
256
- getOwnPropertyDescriptor(target, p) {
257
- return Object.getOwnPropertyDescriptor(target._el._style, p)
258
- },
259
- ownKeys(target) {
260
- return Object.keys(target._el._style)
261
- }
262
- }
263
-
264
- // ---- the el.class proxy --------------------------------------------------------------------------
265
-
266
- // `$checked` and `checked` name the same class — `$` is only the declaration-site prefix in .style().
267
- const classKey = (name: string) => name[0] === "$" ? name.slice(1) : name
268
-
269
- // Single-class state write, shared by the set trap, the batch form and binding re-runs. No-op when
270
- // the state already matches, so an unchanged binding re-run doesn't resend over the bridge.
271
- function writeClass(el: any, name: string, enabled: boolean) {
272
- if ((el._classes?.has(name) ?? false) === enabled) return
273
- if (enabled) {
274
- (el._classes ??= new Set()).add(name)
275
- } else {
276
- el._classes.delete(name)
277
- }
278
- if (el._id !== 0) {
279
- _creatorUI.setClass(el._id, name, enabled)
280
- }
281
- }
282
-
283
- // One class write: a function value installs/replaces a reactive binding, a plain value replaces
284
- // the binding (last write wins — unlike the style proxy's hot-path caveat, a stale class binding
285
- // silently reverting a later plain write would be a footgun, and class flips are never per-frame).
286
- function writeClassValue(el: any, name: string, value: ClassValue) {
287
- name = classKey(name)
288
- if (typeof value === "function") {
289
- createBinding(el, "#class:" + name, () => { writeClass(el, name, !!value()) })
290
- } else {
291
- if (el._fx !== undefined) disposeBinding(el, "#class:" + name)
292
- writeClass(el, name, !!value)
293
- }
294
- }
295
-
296
- // The callable target of the el.class proxy (batch form), bound to the element like styleFunction.
297
- function classFunction(this: any, classes: Record<string, ClassValue>) {
298
- for (const key of Object.keys(classes)) {
299
- writeClassValue(this, key, classes[key])
300
- }
301
- return this
302
- }
303
-
304
- const classHandler: ProxyHandler<any> = {
305
- get(target, key) {
306
- if (typeof key !== "string") return undefined
307
- return target._el._classes?.has(classKey(key)) ?? false
308
- },
309
- set(target, key, value) {
310
- if (typeof key === "string") writeClassValue(target._el, key, value)
311
- return true
312
- },
313
- // Object.keys(el.class) / spread list the ACTIVE class names (inspection + tests).
314
- getOwnPropertyDescriptor(target, p) {
315
- if (typeof p !== "string" || !(target._el._classes?.has(classKey(p)) ?? false)) return undefined
316
- return { value: true, enumerable: true, configurable: true, writable: true }
317
- },
318
- ownKeys(target) {
319
- const classes = target._el._classes
320
- return classes !== undefined ? [ ...classes ] : []
321
- }
322
- }
323
-
324
- /** Reactive style input: every primitive-valued prop also accepts a `() => value` binding that
325
- * re-applies when a signal it read changes. Nested state blocks (onPressed, onLandscape, …) stay
326
- * static — bindings are extracted at the top level only. */
327
- export type Reactive<T> = { [K in keyof T]: NonNullable<T[K]> extends object ? T[K] : T[K] | (() => T[K]) }
328
-
329
- /** User-defined style classes: any `$`-prefixed key in `.style()` declares a state block toggled
330
- * from code via the `el.class` proxy; `duration`/`delay` animate the swap. Class state INHERITS
331
- * down the tree (CSS-`.dark`-on-body style): a class set on a node also activates same-name `$`
332
- * blocks on all descendants within the same root (docs/style-class-cascade-plan.md).
333
- * `$pressed`/`$focused` are reserved — hosts toggle them on press/focus (they beat other
334
- * classes, lose to onPressed/onFocused), so children can react to an ancestor's press.
335
- *
336
- * Instantiated with the node's FULL style `T` — a class block accepts everything the node's
337
- * `.style()` does, like `onLandscape`. Narrowing it wrongly rejects valid props such as
338
- * `$active: { color }`; guarded by tests/ui-types.test.ts. */
339
- export type ClassStyles<T> = { [key: `$${string}`]: T & { duration?: number, delay?: number } }
340
-
341
- /** Value accepted by a `el.class` write: a boolean sets the class, a `() => boolean` binds it —
342
- * the class then tracks the signals the function reads (re-evaluated on change). Reads through
343
- * the proxy always return a plain boolean, never the bound function. */
344
- export type ClassValue = boolean | (() => boolean)
345
-
346
- /** The `el.class` proxy — the runtime switch for `$`-class blocks declared in `.style()`
347
- * (see {@link ClassStyles}). One surface, mirroring `el.style`:
348
- *
349
- * ```ts
350
- * el.class.checked // read: is it active on THIS element? → boolean
351
- * el.class.checked = true // activate (false deactivates)
352
- * el.class.open = !el.class.open // toggle
353
- * el.class.done = () => todo.done.value // reactive binding (signals)
354
- * el.class({ checked: true, done: () => … }) // batch form — returns the element, chainable
355
- * ```
356
- *
357
- * Class names are accepted with or without the declaration-site `$` prefix. `R` is the concrete
358
- * node type, so the batch form chains like `.style()`.
359
- *
360
- * Typing note: property access is `any` because an index signature can't give reads (`boolean`)
361
- * and writes ({@link ClassValue}) different types — the batch form is the fully typed path;
362
- * single-key writes are runtime-coerced with `!!`. */
363
- export type Classes<R> = ((classes: Record<string, ClassValue>) => R) & { [key: string]: any }
364
-
365
- /** Members every UI element shares — element interfaces extend this so docs and types live in one
366
- * place. `S` is the element's style object; `A` is the subset `animateTo` accepts (defaults to `S`). */
367
- export interface UIElementBase<S extends object, A extends object = S> {
368
- // NB: no `name` member here. Declaring it would give every element a property in common with
369
- // the style types (BaseStyle.name), letting TS's weak-type check match a NODE against a legacy
370
- // style-first overload — `UIPager(UIText("x"))` must stay a type error (tests/ui-types.test.ts).
371
- /** Style: `.style({...})` merges (chainable); `el.style.key = v` writes one prop, a `() => v`
372
- * value binds it to signals — see {@link Style}. */
373
- style: Style<this, S>
374
- /** Tween to the target style — meta keys `duration`/`delay`/`loop`/…, see {@link AnimateStyle}. */
375
- animateTo: AnimateStyle<this, A>
376
- /** Tween from the given style to the current one (entrance animations). */
377
- animateFrom: AnimateStyle<this, A>
378
- /** Fires after every layout pass with the parent-relative box. */
379
- onLayout(onLayout: OnLayoutCallback): this
380
- /** Absolute rect in device space, read live (includes scroll); `null` before mount. */
381
- getBoundingClientRect(): BoundingClientRect | null
382
- /** Style-class proxy — read `el.class.checked`, set `el.class.checked = true`, toggle with
383
- * `!el.class.checked`, bind `el.class.done = () => sig.value`, batch/chain `el.class({ … })`.
384
- * Classes cascade to descendants. See {@link Classes}. */
385
- readonly class: Classes<this>
386
- }
387
-
388
- /** {@link UIElementBase} plus the child-management surface every container shares. */
389
- export interface UIContainerBase<S extends object, A extends object = S> extends UIElementBase<S, A> {
390
- /** Append children. */
391
- append(...nodes: UINodeChild[]): this
392
- /** Insert children at `index`. */
393
- insert(index: number, ...nodes: UINodeChild[]): this
394
- /** Remove (unmount) the given children. */
395
- remove(...nodes: UINodeChild[]): this
396
- /** Replace all children — an array, or a function for reactive children. */
397
- setContent(nodes: UINodeChild[] | ChildrenFn): this
398
- /** The children array — mutate via `append`/`insert`/`remove`/`setContent`. */
399
- readonly children: UINodeChild[]
400
- }
401
-
402
- /** Orientation state blocks — override props applied only in landscape / portrait (like a
403
- * `$`-class, keyed by device orientation instead of `el.class`). Same full-`T` rule as class
404
- * blocks. */
405
- export type OrientationStyles<T> = {
406
- onLandscape?: T,
407
- onPortrait?: T,
408
- /** @deprecated Misspelling of `onPortrait` (no second "r"). Kept so existing projects and published
409
- * bundles keep working — hosts still honour it — but new code should use `onPortrait`. */
410
- onPortait?: T,
411
- }
412
-
413
- // NB: do NOT annotate `this: R` on StyleFn. As a parameter it makes R contravariant, turning every
414
- // node type INVARIANT (UIScreen<false> stopped being assignable to UIScreen<boolean>). Runtime
415
- // `this` is already fixed by `styleFunction.bind(this)`; R stays covariant in the return, so
416
- // `.style()` still chains. Guarded by tests/ui-types.test-d.ts.
417
- export type StyleFn <R, T extends object> = ((style: Reactive<T> & OrientationStyles<T> & ClassStyles<T>) => R)
418
-
419
- /** The `el.style` surface: callable — `.style({...})` merges and returns the element for
420
- * chaining — and per-key readable/writable (`el.style.opacity = 0.5`; a `() => value` write
421
- * installs a reactive binding). */
422
- export type Style <R, T extends object> = StyleFn<R,T> & T & OrientationStyles<T> & ClassStyles<T>
423
-
424
- /** The `animateTo`/`animateFrom` surface: target style props plus the flat meta keys below
425
- * (stripped before the commit into the element's style). */
426
- export type AnimateStyle<R, T extends object> = ((style: T & {
427
- /** Tween length in **milliseconds** (default 225). */
428
- duration?: number,
429
- /** Wait before starting, in **milliseconds**. A looping animation delays once, not per cycle. */
430
- delay?: number,
431
- /** Escape hatch: target an internal style layer (the onPressed/orientation mechanism). Not part of the supported surface. */
432
- layer?: number,
433
- /** `false` = animate without writing the targets into the element's style (e.g. a fade-out right before `.hide()`). */
434
- commit?: boolean,
435
- /** Repeat the animation: `true` = forever, a number = that many cycles. A looping animation is an
436
- * effect, not a state change — it never commits, and the element ends back at its base style.
437
- * The loop stops when a later `animateTo`/`animateFrom` runs on the element or it leaves the screen. */
438
- loop?: boolean | number,
439
- /** `"ping-pong"` (default) animates there and back each cycle — no jump. `"restart"` snaps back and
440
- * replays forward — for full-turn spinners (`transform: "rotate(360deg)"`) and shimmers. Only meaningful with `loop`. */
441
- loopMode?: "restart" | "ping-pong",
442
- }) => R)
443
-
444
- /** Meta keys of the animateTo/animateFrom options bag — everything that is not a style target. */
445
- const ANIMATE_META = new Set(["duration", "delay", "layer", "commit", "loop", "loopMode"])
446
-
447
- export type AppearStyle <R, T> = (arg: { from: T, duration?: number, delay?: number }) => R
448
- export type DisappearStyle <R, T> = (arg: { to: T, duration?: number, delay?: number }) => R
449
-
450
- /** `onLayout` payload — the node's box in PARENT-relative coordinates. */
451
- export type OnLayoutCallback = (layout: { left: number, top: number, width: number, height: number }) => void
452
-
453
- /** Web-DOMRect shape on purpose (getBoundingClientRect priors must hold): all eight fields, with
454
- * right/bottom/x/y derived SDK-side — hosts only report [left, top, width, height]. */
455
- export type BoundingClientRect = {
456
- x: number, y: number,
457
- left: number, top: number, right: number, bottom: number,
458
- width: number, height: number,
459
- }
460
-
461
- export class Element<T extends string> {
462
- readonly type: T
463
- /** Author-given semantic name (LeCodes `name`) — see BaseStyle.name. Extracted from the options
464
- * object so it never reaches the layout engine; the renderer reads it off the node. */
465
- readonly name?: string
466
- protected _style: any
467
-
468
- protected _id: number = 0
469
-
470
- protected _styleProxy: any
471
- protected _appear: any
472
- protected _disappear: any
473
- /** @internal Reactive bindings owned by this element (style key / "#text" → effect). The element
474
- * is the effect's only strong owner, so discarding a subtree lets its bindings collect with it. */
475
- _fx?: Map<string, any>
476
-
477
- /** @internal Per-subclass style normalization (SDK-name → wire-name, e.g. UIInput's
478
- * `type: "phone"` → `"tel"`), mutating the style in place before it is stored or sent. Runs on
479
- * every entry point: the first-arg style, `.style()`, and single-key `el.style.k =` writes. */
480
- _mapStyle?(style: any): void
481
-
482
- // Per-type default styles are inlined at each component's construction site (spread UNDER the
483
- // user's style — e.g. UIRow's `flexDirection: "row"`), so every node reaches the host with
484
- // explicit values and hosts keep no per-type style opinions of their own.
485
- constructor(type: T, style: any) {
486
- this.type = type
487
- if (style && typeof style.name === "string" && style.name.length > 0) {
488
- this.name = style.name
489
- delete style.name
490
- }
491
- if (style) this._mapStyle?.(style)
492
- this._style = style
493
- if (style) {
494
- const fns = extractBindings(this, style)
495
- if (fns) applyBindings(this, fns) // sync first run — _style holds concrete values pre-mount
496
- }
497
- // Creation-site capture for the editor preview's element inspector: the host sets the flag
498
- // before running the bundle, and the stack (mapped through the bundle's source map) points at
499
- // the user code that created this element. Off everywhere else — zero cost when unset.
500
- if ((globalThis as any).__lecodesCaptureSites) {
501
- (this as any)._site = new Error("lecodes-site")
502
- }
503
- }
504
-
505
- get style(): any {
506
- if (this._styleProxy) return this._styleProxy
507
- const obj = styleFunction.bind(this)
508
- // @ts-ignore
509
- obj._el = this
510
-
511
- const proxy = new Proxy(obj, handler)
512
- this._styleProxy = proxy
513
- return proxy
514
- }
515
-
516
- /** Tween to the target style and commit it (unless `commit: false` or looping — a loop is an
517
- * effect, not a state change). Meta keys: `duration`/`delay`/`loop`/… — see {@link AnimateStyle}. */
518
- animateTo(style: any): this{
519
- _creatorUI.animateTo(this._id, style)
520
- if (style.commit !== false && !style.loop) {
521
- for (const key in style) {
522
- if (!ANIMATE_META.has(key)) this._style[key] = style[key]
523
- }
524
- }
525
- return this
526
- }
527
- /** Tween FROM the given style to the element's current one (entrance animations). */
528
- animateFrom(style: any): this{
529
- _creatorUI.animateFrom(this._id, style)
530
- return this
531
- }
532
-
533
- /** @internal Active style-class names (stored without the `$`). Kept on the element — not just in
534
- * the layout engine — so hosts re-apply them when a detached node is re-mounted (screens
535
- * re-assign _id on every open). */
536
- protected _classes?: Set<string>
537
-
538
- protected _classProxy?: any
539
-
540
- /** Style-class control — flips the `$`-class blocks declared in `.style()`. One proxy, same
541
- * contract as `el.style`:
542
- *
543
- * - `el.class.checked` — read: is the class active on THIS element? → `boolean`
544
- * - `el.class.checked = true` / `= false` — activate / deactivate
545
- * - `el.class.open = !el.class.open` — toggle
546
- * - `el.class.done = () => todo.done.value` — reactive: the class tracks the signals the
547
- * function reads; a later write to the same class (plain or function) replaces the binding
548
- * - `el.class({ checked: true, done: () => … })` — batch form, returns the element (chainable)
549
- *
550
- * Names are accepted with or without the leading `$`. A class set on an element CASCADES: it is
551
- * also active on every descendant (their same-name `$` blocks light up), until the subtree's
552
- * root — hosted screens/widgets don't inherit. Reads reflect only this element's own classes;
553
- * there is no opt-out below an active ancestor. `Object.keys(el.class)` lists the active names. */
554
- get class(): any {
555
- if (this._classProxy) return this._classProxy
556
- const obj = classFunction.bind(this)
557
- // @ts-ignore
558
- obj._el = this
559
-
560
- const proxy = new Proxy(obj, classHandler)
561
- this._classProxy = proxy
562
- return proxy
563
- }
564
-
565
- protected ll?: OnLayoutCallback[]
566
- /** Observe layout: fires after every host layout pass with the parent-relative box. */
567
- onLayout(onLayout: OnLayoutCallback): this {
568
- if (this.ll) {
569
- this.ll.push(onLayout)
570
- } else {
571
- this.ll = [ onLayout ]
572
- }
573
- return this
574
- }
575
-
576
- /** Absolute rect in device space — the space `UIWidget` positions in and touch events report
577
- * `clientX`/`clientY` in — including scroll offsets, read live at call time (unlike `onLayout`,
578
- * whose coordinates are parent-relative and go stale when an ancestor scrolls). `null` before
579
- * the element is mounted and on hosts without the bridge read. Anchor popovers position-once
580
- * at open; don't poll per frame. */
581
- getBoundingClientRect(): BoundingClientRect | null {
582
- if (this._id === 0) return null
583
- const rect = _creatorUI.getBoundingClientRect?.(this._id)
584
- if (!rect) return null
585
- const [left, top, width, height] = rect
586
- return { x: left, y: top, left, top, right: left + width, bottom: top + height, width, height }
587
- }
588
- }
589
-
590
- // ---- reactive children --------------------------------------------------------------------------
591
-
592
- /** Reactive children: re-runs when a signal it read changes; the result is reconciled against
593
- * the mounted children (kept nodes stay mounted, state intact). */
594
- export type ChildrenFn = () => UINodeChild[]
595
-
596
- // The container whose children binding is currently executing. __uiMap uses it as the cache owner;
597
- // nested bindings save/restore it, so each level caches on its own element.
598
- let currentMapOwner: any = null
599
-
600
- /**
601
- * Compiler-emitted memoized map for reactive children. The chisel `reactiveUi` pass rewrites
602
- * `list.map(fn)` inside a children binding to `__uiMap(list, fn, "<file>:<offset>")`, so unchanged
603
- * items keep their element identity across re-runs (the reconciler then leaves them mounted).
604
- * Cache: per (binding owner, call-site slot), keyed by item reference; duplicate items consume
605
- * cached nodes in order. Outside a children binding, for non-array inputs, or for non-element
606
- * results it behaves exactly like Array.prototype.map — a wrong rewrite can't change behavior.
607
- */
608
- export function __uiMap(list: any, render: (item: any, index: number) => any, slot: string): any[] {
609
- if (currentMapOwner === null || !Array.isArray(list)) {
610
- return list?.map(render) ?? []
611
- }
612
- const slots: Map<string, Map<any, any[]>> = currentMapOwner._mapCache ??= new Map()
613
- const prev = slots.get(slot)
614
- const next = new Map<any, any[]>()
615
- const out: any[] = []
616
- for (let i = 0; i < list.length; i++) {
617
- const item = list[i]
618
- const stack = prev?.get(item)
619
- let node = stack !== undefined && stack.length > 0 ? stack.shift() : undefined
620
- if (node === undefined) {
621
- node = render(item, i)
622
- if (!(node instanceof Element)) {
623
- out.push(node) // not a UI element — plain map semantics, never cached
624
- continue
625
- }
626
- }
627
- let arr = next.get(item)
628
- if (arr === undefined) next.set(item, arr = [])
629
- arr.push(node)
630
- out.push(node)
631
- }
632
- // entries left in `prev` (items gone from the list) are dropped here; their nodes and bindings
633
- // become unreachable and collect
634
- slots.set(slot, next)
635
- return out
636
- }
637
-
638
- // ---- factory argument dispatch ------------------------------------------------------------------
639
-
640
- /** @internal Shared argument dispatch for the UI factories (`UIRow(...)`, `UIButton(...)`, …).
641
- * Children are variadic; an array argument is flattened one level, so both the legacy
642
- * `UIColumn([a, b])` and `UIColumn(header, items.map(row), footer)` work. A lone function
643
- * argument is a reactive {@link ChildrenFn}. A plain non-node object in first position is the
644
- * legacy style bag: it is spread over `defaults`, or — when `defaults` is null — stored as-is
645
- * (the Element constructor mutates it in place, matching the old path). The rest-args array is
646
- * taken over as the children array when nothing needs flattening — callers must hand it off. */
647
- export function buildUI<R>(args: any[], defaults: any, make: (style: any, children: any[] | ChildrenFn) => R): R {
648
- const n = args.length
649
- const a0 = n > 0 ? args[0] : undefined
650
- let style: any
651
- let start = 0
652
- if (a0 !== null && typeof a0 === "object" && !Array.isArray(a0) && !(a0 instanceof Element)) {
653
- style = defaults ? { ...defaults, ...a0 } : a0
654
- start = 1
655
- } else {
656
- style = defaults ? { ...defaults } : {}
657
- }
658
- if (n === start + 1) {
659
- const a = args[start]
660
- // lone function → reactive children; lone array → used as the children array directly
661
- // (the legacy zero-copy path — nested arrays inside it are NOT flattened)
662
- if (typeof a === "function" || Array.isArray(a)) return make(style, a)
663
- }
664
- // Single pass: reuse the rest-args array as the children array until the first array argument
665
- // shows up, then copy the prefix once and flatten from there. Falsy entries stay — they are
666
- // compacted at mount like always.
667
- let flat: any[] | undefined
668
- for (let i = start; i < n; i++) {
669
- const a = args[i]
670
- if (flat === undefined) {
671
- if (Array.isArray(a)) {
672
- flat = args.slice(start, i)
673
- for (let j = 0; j < a.length; j++) flat.push(a[j])
674
- }
675
- } else if (Array.isArray(a)) {
676
- for (let j = 0; j < a.length; j++) flat.push(a[j])
677
- } else {
678
- flat.push(a)
679
- }
680
- }
681
- if (flat !== undefined) return make(style, flat)
682
- return make(style, start === 0 ? args : args.slice(1))
683
- }
684
-
685
- export class ContainerElement<T extends string> extends Element<T> {
686
- children: any[] = []
687
-
688
- constructor(type: T, style: any, children: any[] | ChildrenFn) {
689
- super(type, style)
690
- if (typeof children === "function") {
691
- this._bindChildren(children)
692
- } else {
693
- this.children = children
694
- }
695
- }
696
-
697
- /** @internal Reactive children: re-runs when a signal read inside `fn` changes and reconciles
698
- * the result against the mounted children with minimal insert/remove ops. */
699
- _bindChildren(fn: ChildrenFn) {
700
- createBinding(this, "#children", () => {
701
- const prevOwner = currentMapOwner
702
- currentMapOwner = this
703
- let next: UINodeChild[]
704
- try {
705
- next = fn()
706
- } finally {
707
- currentMapOwner = prevOwner
708
- }
709
- this._reconcile(Array.isArray(next) ? next : [])
710
- })
711
- }
712
-
713
- /** @internal Identity diff of the current children against `next`: departed nodes are removed,
714
- * new ones inserted at their position, kept nodes stay mounted (so their host state survives).
715
- * A moved node is remove+insert — it remounts. Falsy entries are compacted away. */
716
- _reconcile(next: UINodeChild[]) {
717
- const target: any[] = []
718
- for (const c of next) if (c) target.push(c)
719
-
720
- if (this._id === 0) {
721
- this.children.length = 0
722
- this.children.push(...target)
723
- return
724
- }
725
-
726
- // compact holes left by a previous static children array (falsy entries were never mounted)
727
- for (let i = this.children.length - 1; i >= 0; i--) {
728
- if (!this.children[i]) this.children.splice(i, 1)
729
- }
730
-
731
- const targetSet = new Set(target)
732
- const departed = this.children.filter(c => !targetSet.has(c))
733
- if (departed.length > 0) this.remove(...departed)
734
-
735
- // this.children now mirrors the mounted order; walk the target and fix mismatches in place
736
- for (let i = 0; i < target.length; i++) {
737
- if (this.children[i] === target[i]) continue
738
- const node = target[i]
739
- if (this.children.includes(node, i + 1)) {
740
- this.remove(node) // moved forward — remount at its new position
741
- }
742
- this.insert(i, node)
743
- }
744
- }
745
-
746
- /** Append children (falsy entries are kept in the array but never mounted). */
747
- append(...nodes: UINodeChild[]): this {
748
- if (this._id === 0) {
749
- this.children.push(...nodes)
750
- return this
751
- }
752
- let index = 0
753
- for (let child of this.children) {
754
- if (child) {
755
- index++
756
- }
757
- }
758
- for (let node of nodes) {
759
- if (!node) continue
760
- _creatorUI.insertNode(index, this._id, node)
761
- index++
762
- }
763
- this.children.push(...nodes)
764
- return this
765
- }
766
- /** Insert children at `index` of the children array. */
767
- insert(index: number, ...nodes: UINodeChild[]): this {
768
- if (this._id === 0) {
769
- this.children.splice(index, 0, ...nodes)
770
- return this
771
- }
772
-
773
- let _index = 0
774
- for (let i = 0; i < index; i++) {
775
- if (this.children[i]) {
776
- _index++
777
- }
778
- }
779
- for (let node of nodes) {
780
- if (!node) continue
781
- _creatorUI.insertNode(_index, this._id, node)
782
- _index++
783
- }
784
- this.children.splice(index, 0, ...nodes)
785
- return this
786
- }
787
- /** Remove (unmount) the given children. */
788
- remove(...nodesToDelete: UINode[]): this {
789
- const set = new Set(nodesToDelete)
790
-
791
- for (let i = 0; i < this.children.length; i++) {
792
- if (set.has(this.children[i])) {
793
- set.delete(this.children[i])
794
- this.children.splice(i, 1)
795
- i--
796
-
797
- if (set.size === 0) {
798
- break
799
- }
800
- }
801
- }
802
-
803
- if (this._id === 0) {
804
- return this
805
- }
806
- for (let node of nodesToDelete) {
807
- _creatorUI.removeNode(this._id, node)
808
- }
809
- return this
810
- }
811
-
812
- /** Replace all children — a plain array, or a function for reactive children. */
813
- setContent(children: UINodeChild[] | ChildrenFn): this {
814
- if (typeof children === "function") {
815
- this._bindChildren(children) // createBinding replaces a previous "#children" binding
816
- return this
817
- }
818
- disposeBinding(this, "#children") // static content overrides a reactive binding
819
- if (this._id !== 0) {
820
- _creatorUI.setContent(this._id, this.children, children)
821
- }
822
- this.children.length = 0
823
- this.children.push(...children)
824
- return this
825
- }
826
- }