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.
- package/README.md +5 -2
- package/dist/editor.d.ts +12 -0
- package/dist/global.d.ts +4 -8
- package/dist/host.d.ts +2 -3
- package/dist/types/animate/tween/Animation.d.ts +0 -3
- package/dist/types/animate/tween/animateValue.d.ts +4 -2
- package/dist/types/animate/tween/easing.d.ts +8 -0
- package/dist/types/animate/tween/spec.d.ts +15 -7
- package/dist/types/audio/audio.d.ts +2 -1
- package/dist/types/canvas/Canvas.d.ts +40 -108
- package/dist/types/canvas/gen/cssColor.d.ts +17 -0
- package/dist/types/canvas/gen/recorder.d.ts +118 -0
- package/dist/types/canvas/gen/spec.d.ts +144 -0
- package/dist/types/core/color.d.ts +3 -1
- package/dist/types/core/pins.d.ts +18 -0
- package/dist/types/g2/Node2D.d.ts +5 -8
- package/dist/types/g2/Scene2D.d.ts +6 -2
- package/dist/types/gl/Foliage.d.ts +30 -7
- package/dist/types/gl/Light.d.ts +8 -0
- package/dist/types/gl/Lightmap.d.ts +13 -2
- package/dist/types/gl/Material.d.ts +17 -3
- package/dist/types/gl/Model.d.ts +6 -2
- package/dist/types/gl/Node.d.ts +3 -6
- package/dist/types/gl/Scene.d.ts +41 -13
- package/dist/types/gl/Texture.d.ts +1 -1
- package/dist/types/gl/animation/Locomotion.d.ts +8 -1
- package/dist/types/inject.d.ts +11 -11
- package/dist/types/inject.editor.d.ts +1 -0
- package/dist/types/net/core.d.ts +7 -0
- package/dist/types/plugin.d.ts +76 -0
- package/dist/types/plugins/gen/camera/sdk/camera.d.ts +24 -0
- package/dist/types/plugins/gen/camera/sdk/camera.gen.d.ts +25 -0
- package/dist/types/plugins/{geolocation.d.ts → gen/geolocation/sdk/geolocation.d.ts} +2 -20
- package/dist/types/plugins/gen/geolocation/sdk/geolocation.gen.d.ts +31 -0
- package/dist/types/plugins/{map.d.ts → gen/map/sdk/map.d.ts} +9 -50
- package/dist/types/plugins/gen/map/sdk/map.gen.d.ts +53 -0
- package/dist/types/plugins/gen/push/sdk/push.d.ts +23 -0
- package/dist/types/plugins/gen/push/sdk/push.gen.d.ts +35 -0
- package/dist/types/plugins/{qr.d.ts → gen/qr-scanner/sdk/qr-scanner.d.ts} +2 -3
- package/dist/types/plugins/gen/qr-scanner/sdk/qr-scanner.gen.d.ts +15 -0
- package/dist/types/runtime/app.d.ts +9 -2
- package/dist/types/runtime/fetch.d.ts +2 -0
- package/dist/types/runtime/input.d.ts +1 -1
- package/dist/types/runtime/media.d.ts +6 -10
- package/dist/types/runtime/misc.d.ts +4 -1
- package/dist/types/runtime/net.d.ts +3 -2
- package/dist/types/runtime/touch.d.ts +32 -0
- package/dist/types/scene/defineScene.d.ts +43 -2
- package/dist/types/scene/editor.d.ts +52 -0
- package/dist/types/scene/gizmos.d.ts +7 -4
- package/dist/types/ui/NativeView.d.ts +6 -4
- package/dist/types/ui/UI.d.ts +1 -1
- package/dist/types/ui/UIBottomSheet.d.ts +6 -12
- package/dist/types/ui/UIButton.d.ts +12 -14
- package/dist/types/ui/UIContainer.d.ts +0 -6
- package/dist/types/ui/UIImage.d.ts +1 -4
- package/dist/types/ui/UIInput.d.ts +8 -24
- package/dist/types/ui/UIModal.d.ts +0 -2
- package/dist/types/ui/UINode.d.ts +70 -56
- package/dist/types/ui/UIPager.d.ts +28 -27
- package/dist/types/ui/UIPopover.d.ts +0 -2
- package/dist/types/ui/UIScreen.d.ts +12 -17
- package/dist/types/ui/UIScrollable.d.ts +1 -4
- package/dist/types/ui/UIText.d.ts +0 -2
- package/dist/types/ui/UIVideo.d.ts +3 -5
- package/dist/types/ui/UIVirtualizedList.d.ts +14 -16
- package/dist/types/ui/UIWidget.d.ts +10 -9
- package/dist/types/ui/colorKeys.gen.d.ts +9 -0
- package/dist/types/ui/presentable.d.ts +46 -32
- package/dist/types/ui/router.d.ts +18 -7
- package/dist/types/ui/styleColor.d.ts +1 -0
- package/dist/types/ui/transitions.d.ts +18 -0
- package/dist/types/ui/tree.d.ts +75 -0
- package/dist/types/version.d.ts +10 -0
- package/dist/types.json +1 -1
- package/package.json +12 -3
- package/prompts/2d.md +2 -6
- package/prompts/3d.md +1 -5
- package/prompts/README.md +1 -1
- package/prompts/canvas.md +9 -8
- package/prompts/compose.ts +1 -1
- package/prompts/core.md +3 -3
- package/prompts/design.md +1 -1
- package/prompts/dist/2d-game.md +473 -239
- package/prompts/dist/3d-app.md +553 -205
- package/prompts/dist/ar-app.md +435 -202
- package/prompts/dist/design.md +113 -95
- package/prompts/dist/ui-app.md +386 -170
- package/prompts/ui-design.md +2 -3
- package/prompts/ui.md +45 -37
- package/src/animate/tween/Animation.ts +34 -150
- package/src/animate/tween/Timeline.ts +175 -175
- package/src/animate/tween/animateValue.ts +6 -3
- package/src/animate/tween/easing.ts +10 -3
- package/src/animate/tween/spec.ts +41 -15
- package/src/audio/Sound.ts +3 -3
- package/src/audio/audio.ts +2 -1
- package/src/bridges/2d.d.ts +317 -0
- package/src/bridges/app.d.ts +91 -0
- package/src/bridges/audio.d.ts +97 -0
- package/src/bridges/canvas.d.ts +79 -0
- package/src/bridges/device.d.ts +72 -0
- package/src/bridges/fetch.d.ts +80 -0
- package/src/bridges/files.d.ts +70 -0
- package/src/bridges/gl.d.ts +1133 -0
- package/src/bridges/input.d.ts +72 -0
- package/src/bridges/media.d.ts +55 -0
- package/src/bridges/nav.d.ts +71 -0
- package/src/bridges/net.d.ts +52 -0
- package/src/bridges/service.d.ts +52 -0
- package/src/bridges/socket.d.ts +31 -0
- package/src/bridges/storage.d.ts +33 -0
- package/src/bridges/tree.d.ts +301 -0
- package/src/bridges/types.d.ts +49 -0
- package/src/canvas/Canvas.ts +114 -159
- package/src/canvas/gen/cssColor.ts +224 -0
- package/src/canvas/gen/recorder.ts +212 -0
- package/src/canvas/gen/spec.ts +201 -0
- package/src/chisel.ts +193 -0
- package/src/compile/assetMacro.ts +1 -1
- package/src/compile/bundler.ts +11 -2
- package/src/compile/compileProject.ts +43 -4
- package/src/compile/fontMacro.ts +3 -4
- package/src/compile/header.ts +26 -5
- package/src/compile/index.ts +3 -1
- package/src/compile/liteMaterial.ts +1 -1
- package/src/compile/sceneEditor.ts +11 -26
- package/src/core/color.ts +73 -30
- package/src/core/pins.ts +51 -0
- package/src/core/signals.ts +8 -1
- package/src/g2/CharacterController2D.ts +3 -3
- package/src/g2/Node2D.ts +57 -39
- package/src/g2/Physics2D.ts +2 -2
- package/src/g2/Scene2D.ts +35 -23
- package/src/g2/Texture2D.ts +1 -1
- package/src/g2/loop.ts +4 -4
- package/src/gl/CameraPlace.ts +52 -52
- package/src/gl/Foliage.ts +72 -17
- package/src/gl/Geometry.ts +1 -2
- package/src/gl/Light.ts +10 -0
- package/src/gl/Lightmap.ts +45 -29
- package/src/gl/Material.ts +95 -51
- package/src/gl/Mesh.ts +120 -120
- package/src/gl/Model.ts +21 -16
- package/src/gl/Node.ts +91 -24
- package/src/gl/Particles.ts +1 -1
- package/src/gl/Scene.ts +100 -44
- package/src/gl/Texture.ts +8 -7
- package/src/gl/animation/AnimationClip.ts +1 -1
- package/src/gl/animation/DynamicBone.ts +482 -482
- package/src/gl/animation/Locomotion.ts +8 -3
- package/src/gl/nav/NavMesh.ts +3 -4
- package/src/gl/physics/Physics.ts +2 -2
- package/src/gl/physics/physicsEvents.ts +3 -3
- package/src/gl/scenarios.ts +291 -291
- package/src/gl/terrain/Terrain.ts +4 -5
- package/src/gl/touch.ts +14 -15
- package/src/host.d.ts +2 -3
- package/src/inject.editor.ts +7 -0
- package/src/inject.ts +13 -16
- package/src/net/core.ts +6 -5
- package/src/net/index.ts +1 -1
- package/src/net/replication.ts +1 -1
- package/src/plugin.ts +191 -0
- package/src/plugins/gen/camera/contract.d.ts +27 -0
- package/src/plugins/gen/camera/sdk/camera.gen.ts +46 -0
- package/src/plugins/gen/camera/sdk/camera.ts +57 -0
- package/src/plugins/gen/geolocation/contract.d.ts +50 -0
- package/src/plugins/gen/geolocation/sdk/geolocation.gen.ts +54 -0
- package/src/plugins/{geolocation.ts → gen/geolocation/sdk/geolocation.ts} +22 -43
- package/src/plugins/gen/map/contract.d.ts +144 -0
- package/src/plugins/gen/map/sdk/map.gen.ts +88 -0
- package/src/plugins/{map.ts → gen/map/sdk/map.ts} +68 -102
- package/src/plugins/gen/push/contract.d.ts +61 -0
- package/src/plugins/gen/push/sdk/push.gen.ts +60 -0
- package/src/plugins/gen/push/sdk/push.ts +105 -0
- package/src/plugins/gen/qr-scanner/contract.d.ts +16 -0
- package/src/plugins/gen/qr-scanner/sdk/qr-scanner.gen.ts +29 -0
- package/src/plugins/gen/qr-scanner/sdk/qr-scanner.ts +52 -0
- package/src/plugins/permission.ts +5 -4
- package/src/runtime/app.ts +20 -9
- package/src/runtime/appEvents.ts +5 -4
- package/src/runtime/channel.ts +18 -15
- package/src/runtime/clipboard.ts +4 -3
- package/src/runtime/datetime.ts +2 -1
- package/src/runtime/device.ts +17 -15
- package/src/runtime/fetch.ts +30 -20
- package/src/runtime/files.ts +16 -15
- package/src/runtime/input.ts +12 -10
- package/src/runtime/media.ts +50 -46
- package/src/runtime/misc.ts +7 -3
- package/src/runtime/net.ts +8 -7
- package/src/runtime/rpc.ts +1 -3
- package/src/runtime/service.ts +19 -14
- package/src/runtime/share.ts +4 -3
- package/src/runtime/storage.ts +6 -4
- package/src/runtime/touch.ts +32 -0
- package/src/scene/defineScene.ts +61 -365
- package/src/scene/editor.ts +408 -0
- package/src/scene/editorPlugins.ts +3 -3
- package/src/scene/gizmos.ts +15 -9
- package/src/server/db/marci/query.ts +1 -1
- package/src/server/host.ts +1 -1
- package/src/server/runtime.ts +1 -1
- package/src/ui/NativeView.ts +72 -25
- package/src/ui/UI.ts +3 -3
- package/src/ui/UIBottomSheet.ts +16 -17
- package/src/ui/UIButton.ts +54 -16
- package/src/ui/UIContainer.ts +0 -6
- package/src/ui/UIImage.ts +34 -30
- package/src/ui/UIInput.ts +29 -37
- package/src/ui/UIModal.ts +1 -3
- package/src/ui/UINode.ts +347 -297
- package/src/ui/UIPager.ts +93 -78
- package/src/ui/UIPopover.ts +0 -2
- package/src/ui/UIScreen.ts +41 -36
- package/src/ui/UIScrollable.ts +19 -13
- package/src/ui/UISpacer.ts +1 -1
- package/src/ui/UITabs.ts +8 -6
- package/src/ui/UIText.ts +7 -17
- package/src/ui/UIVideo.ts +30 -27
- package/src/ui/UIVirtualizedList.ts +58 -59
- package/src/ui/UIWidget.ts +38 -18
- package/src/ui/colorKeys.gen.ts +37 -0
- package/src/ui/fonts.ts +2 -2
- package/src/ui/presentable.ts +59 -42
- package/src/ui/router.ts +52 -35
- package/src/ui/styleColor.ts +56 -0
- package/src/ui/theme.ts +7 -6
- package/src/ui/transitions.ts +249 -0
- package/src/ui/tree.ts +346 -0
- package/src/version.ts +24 -0
- package/tests/helpers/engineWorld.ts +11 -0
- package/tests/helpers/fakeTree.ts +353 -0
- package/tests/helpers/hostStubs.ts +31 -0
- package/tests/helpers/index.ts +14 -0
- package/tests/helpers/memoryMarci.ts +124 -0
- package/tests/helpers/phases.ts +23 -0
- package/tests/helpers/preload.ts +18 -0
- package/tests/helpers/stubApp.ts +2 -0
- package/tests/helpers/stubDevice.ts +2 -0
- package/tests/helpers/stubFetch.ts +2 -0
- package/tests/helpers/stubInput.ts +2 -0
- package/dist/inject.js +0 -4629
- package/dist/types/core/registry.d.ts +0 -7
- package/dist/types/plugins/camera.d.ts +0 -25
- package/dist/types/plugins/push.d.ts +0 -46
- package/src/bridges.d.ts +0 -1769
- package/src/compile/__tests__/assetIconMacro.test.ts +0 -219
- package/src/compile/__tests__/assetMacro.test.ts +0 -100
- package/src/compile/__tests__/assetName.test.ts +0 -55
- package/src/compile/__tests__/compile.test.ts +0 -310
- package/src/compile/__tests__/detectEntry.test.ts +0 -151
- package/src/compile/__tests__/fontMacro.test.ts +0 -199
- package/src/compile/__tests__/serverSplit.test.ts +0 -27
- package/src/core/__tests__/stateMachine.test.ts +0 -132
- package/src/core/registry.ts +0 -23
- package/src/plugins/camera.ts +0 -81
- package/src/plugins/push.ts +0 -132
- 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
|
-
|
|
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
|
|
25
|
-
*
|
|
26
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
218
|
-
|
|
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.
|
|
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
|
-
|
|
258
|
-
|
|
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.
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
/**
|
|
463
|
-
*
|
|
464
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
487
|
-
|
|
488
|
-
|
|
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
|
-
|
|
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 (
|
|
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.
|
|
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.
|
|
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.
|
|
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 `$`)
|
|
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
|
|
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.
|
|
612
|
-
const rect =
|
|
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
|
|
671
|
-
* `UIColumn(
|
|
672
|
-
*
|
|
673
|
-
*
|
|
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
|
|
679
|
-
|
|
680
|
-
|
|
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
|
|
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 =
|
|
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(
|
|
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
|
-
|
|
711
|
-
return make(style, start === 0 ? args : args.slice(1))
|
|
770
|
+
return make(style, flat !== undefined ? flat : args)
|
|
712
771
|
}
|
|
713
772
|
|
|
714
|
-
|
|
715
|
-
|
|
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
|
|
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 =
|
|
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
|
-
//
|
|
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 (
|
|
839
|
+
if (cur[i] === target[i]) continue
|
|
767
840
|
const node = target[i]
|
|
768
|
-
|
|
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.
|
|
846
|
+
this._insertAt(i, node)
|
|
847
|
+
cur.splice(i, 0, node)
|
|
772
848
|
}
|
|
773
849
|
}
|
|
774
850
|
|
|
775
|
-
|
|
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.
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
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
|
|
868
|
+
/** Insert children at `index`. */
|
|
796
869
|
insert(index: number, ...nodes: UINodeChild[]): this {
|
|
797
|
-
if (this.
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
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
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
for (
|
|
821
|
-
if (
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
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
|
-
|
|
849
|
-
|
|
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
|
+
}
|