lecodes-sdk 2.0.7 → 2.0.9

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 (68) hide show
  1. package/dist/global.d.ts +6 -0
  2. package/dist/types/g2/Scene2D.d.ts +4 -0
  3. package/dist/types/gl/Light.d.ts +36 -1
  4. package/dist/types/gl/Scene.d.ts +4 -0
  5. package/dist/types/inject.d.ts +2 -1
  6. package/dist/types/runtime/rpc.d.ts +2 -0
  7. package/dist/types/server/context.d.ts +4 -0
  8. package/dist/types/server/db/defineDb.d.ts +1 -1
  9. package/dist/types/server/db/fields.d.ts +25 -1
  10. package/dist/types/server/db/marci/query.d.ts +39 -2
  11. package/dist/types/server/db/types.d.ts +13 -7
  12. package/dist/types/server/files/db.d.ts +39 -0
  13. package/dist/types/server/files/models.d.ts +131 -0
  14. package/dist/types/server/inject.d.ts +1 -0
  15. package/dist/types/ui/NativeView.d.ts +3 -0
  16. package/dist/types/ui/UI.d.ts +1 -0
  17. package/dist/types/ui/UILayer.d.ts +25 -0
  18. package/dist/types/ui/UIModal.d.ts +20 -16
  19. package/dist/types/ui/UIPopover.d.ts +5 -3
  20. package/dist/types/ui/UIVideo.d.ts +3 -0
  21. package/dist/types/ui/UIWidget.d.ts +25 -24
  22. package/dist/types/ui/presentable.d.ts +10 -0
  23. package/dist/types/ui/transitions.d.ts +2 -1
  24. package/dist/types/ui/tree.d.ts +1 -0
  25. package/dist/types/version.d.ts +1 -1
  26. package/dist/types.json +1 -1
  27. package/package.json +3 -2
  28. package/prompts/3d-scene-files.md +3 -3
  29. package/prompts/3d-scene.md +1 -1
  30. package/prompts/dist/3d-app.md +4 -4
  31. package/src/bridges/gl.d.ts +5 -0
  32. package/src/bridges/tree.d.ts +24 -12
  33. package/src/chisel.ts +1 -1
  34. package/src/compile/libraryImports.ts +9 -0
  35. package/src/compile/serverTypes.ts +6 -0
  36. package/src/g2/Scene2D.ts +10 -0
  37. package/src/gl/Light.ts +78 -2
  38. package/src/gl/Material.ts +3 -0
  39. package/src/gl/Scene.ts +11 -1
  40. package/src/inject.ts +2 -0
  41. package/src/runtime/rpc.ts +15 -3
  42. package/src/server/context.ts +4 -0
  43. package/src/server/db/defineDb.ts +62 -9
  44. package/src/server/db/fields.ts +21 -1
  45. package/src/server/db/httpTransport.ts +9 -1
  46. package/src/server/db/marci/query.ts +71 -4
  47. package/src/server/db/types.ts +16 -5
  48. package/src/server/files/db.ts +182 -0
  49. package/src/server/files/host.ts +496 -0
  50. package/src/server/files/models.ts +116 -0
  51. package/src/server/host.ts +17 -4
  52. package/src/server/inject.ts +1 -0
  53. package/src/server/runtime.ts +41 -0
  54. package/src/server/validate.ts +5 -0
  55. package/src/ui/NativeView.ts +11 -0
  56. package/src/ui/UI.ts +1 -0
  57. package/src/ui/UIBottomSheet.ts +8 -7
  58. package/src/ui/UILayer.ts +88 -0
  59. package/src/ui/UIModal.ts +59 -43
  60. package/src/ui/UINode.ts +9 -1
  61. package/src/ui/UIPopover.ts +23 -9
  62. package/src/ui/UIVideo.ts +11 -0
  63. package/src/ui/UIWidget.ts +70 -44
  64. package/src/ui/presentable.ts +11 -1
  65. package/src/ui/transitions.ts +36 -7
  66. package/src/ui/tree.ts +4 -0
  67. package/src/version.ts +1 -1
  68. package/tests/helpers/fakeTree.ts +7 -3
@@ -1,42 +1,45 @@
1
1
  import { TouchStartEvent } from "../runtime/touch"
2
- import { Presentable } from "./presentable"
3
2
  import { ContainerElement, buildUI, type BaseStyle, type Color, type ContainerStyle, type DrawableStyle, type PaddingStyle, type PositionStyle, type UIChildArg, type UIContainerBase, type UINodeChild, type ChildrenFn } from "./UINode"
4
- import { TREE_DEST_NONE, TREE_FLAG_BACK, TREE_FLAG_INTERACTIVE, TREE_FLAG_OVERLAY_TAP, pin, tree, unpin } from "./tree"
3
+ import { UIOverlay } from "./UILayer"
4
+ import { TREE_FLAG_BACK, TREE_FLAG_INTERACTIVE, TREE_FLAG_OVERLAY_TAP, tree } from "./tree"
5
5
 
6
6
 
7
7
  export type UIWidgetStyle = ContainerStyle & DrawableStyle & PaddingStyle & BaseStyle & PositionStyle & {
8
+ /** Shown (`"flex"`, the default) or not: what `show()` / `hide()` write, what a state block may
9
+ * flip (`$open: { display: "flex" }`). */
10
+ display?: "none" | "flex",
8
11
  /** A layer behind the widget that intercepts clicks — `"transparent"` still intercepts, `null`
9
12
  * removes the layer. A tap on it fires `onOverlayTap`. */
10
13
  overlayColor?: Color | null
11
14
  }
12
15
 
13
16
 
14
- /** A floating overlay — absolutely positioned in device coordinates, mounted above the page as
15
- * its own layout root (never a child of what's underneath). The base of dialogs, toasts,
16
- * mini-players and scene HUDs; `UIModal`/`UIPopover`/`UIBottomSheet` build on it. */
17
+ /** A floating overlay that belongs to a WINDOW, not to a box (docs/plans/widgets-plan.md): a dialog,
18
+ * a sheet, a HUD, a mini-player. A widget is a CHILD of the place it belongs to — a `UIScreen`
19
+ * (among its children), a surface's content (`scene.setContent([hud])`) or the app layer
20
+ * (`UIOverlay`) — and is laid out and painted in the box of the outermost presented destination
21
+ * that contains it: absolutely positioned in that box, above everything else in it. A widget on a
22
+ * tab page covers the tab bar and rides the shell's push and pop. A widget inside a `UIColumn` or
23
+ * any other box is an error: a floating element of a box is an absolute child, not a widget.
24
+ *
25
+ * It shows with its place unless its style says `display: "none"`; `show()` / `hide()` flip that,
26
+ * and so does a state block (`$class: { display: "flex" }`). `UIModal` / `UIPopover` /
27
+ * `UIBottomSheet` build on it. */
17
28
  export interface UIWidget extends UIContainerBase<UIWidgetStyle, DrawableStyle & BaseStyle & { overlayColor?: Color | null }> {
18
29
  readonly type: "widget",
19
30
 
20
- /**
21
- * Attach this widget to a destination (a `Scene`, a `UIScreen`, …): the widget lives on that
22
- * page's layer — it shows/hides with the page and rides its transition, and a screen pushed
23
- * on top covers it (a scene HUD behaves like part of the scene). Sticky until `attachTo(null)`
24
- * (= back to a global overlay). Set it while the widget is hidden. The widget is always its own
25
- * overlay root mounted into the owner's page view — it never becomes a child of the owner, so it
26
- * stays topmost and its updates never trigger the owner's relayout. Works with a `UIScreen` that
27
- * is a page inside a `UIPager`: the widget mounts into that tab and rides its swipe.
28
- */
29
- attachTo(owner: Presentable | null): this,
30
-
31
- /** Show the widget. By default it is a global overlay above every destination — it stays
32
- * visible across navigation until `hide()` (dialogs, mini-player, global loader). If the
33
- * widget was attached via `attachTo(owner)`, it mounts on that page's layer instead and is
34
- * only visible while the owner is presented. */
31
+ /** Show the widget: `display: "flex"`, on top of its root — it moves to the end of its parent's
32
+ * children, so a dialog opened later covers an earlier one and a HUD declared after it. A widget
33
+ * with no parent is appended to the app layer first (`UIOverlay`) — a global overlay above every
34
+ * destination, up until `hide()`. */
35
35
  show(): void,
36
- /** Unmount the widget. */
36
+ /** Hide the widget: `display: "none"`. It stays where it is (its place, the app layer), ready
37
+ * for the next `show()`. */
37
38
  hide(): void,
38
39
 
39
- /** Mounted right now (`show()` ran, `hide()` hasn't). */
40
+ /** Shown right now by its own display (`show()` ran, `hide()` hasn't, no state block hides
41
+ * it). True for a widget hidden WITH its page or its covered screen: that is its place's
42
+ * visibility, not the widget's. */
40
43
  readonly isShown: boolean,
41
44
 
42
45
  /** Touch began on the widget; `ev.track(...)` takes over the rest of the gesture. */
@@ -53,41 +56,63 @@ export interface UIWidget extends UIContainerBase<UIWidgetStyle, DrawableStyle &
53
56
 
54
57
  export class WidgetElement extends ContainerElement<"widget"> {
55
58
  scrollable = false as const
56
- _owner?: Presentable
57
- private _shown = false
59
+ /** @internal The widget's own display as last known, while it has a place: what the app wrote,
60
+ * corrected by the runtime's report when a state block flipped it (TREE_EVENT_WIDGET_VISIBLE).
61
+ * False while the widget is nowhere (made, not placed; removed). */
62
+ _shown = false
58
63
  constructor(type: "widget", style: UIWidgetStyle, children: UINodeChild[] | ChildrenFn) {
59
- // Widgets are absolutely positioned in device coordinates — in the constructor (not the
64
+ // Widgets are absolutely positioned in their root's box — in the constructor (not the
60
65
  // factory) so subclasses like ModalElement inherit it.
61
66
  super(type, { position: "absolute", ...style }, children)
62
67
  }
63
- attachTo(owner: Presentable | null): this {
64
- this._owner = owner ?? undefined
65
- return this
66
- }
67
68
  show(): void {
68
- // A widget is always its OWN overlay root (a separate Yoga tree mounted above the owner's
69
- // content) — never a child of the owner, so it stays topmost, its updates never relayout the
70
- // owner, and it doesn't pollute the owner's children. `attachTo(owner)` just tells the host
71
- // which page view to mount that root into; ownerless widgets are global overlays.
72
- if (this._h.id === 0 || this._shown) return
73
- this._shown = true
74
- pin(this)
75
- if (this._owner) {
76
- const [kind, id] = this._owner._dest()
77
- tree().showWidget(this._h.id, kind, id)
69
+ if (this._h.id === 0) return
70
+ const parentId = tree().parent(this._h.id)
71
+ if (parentId !== 0 && this._shown) return
72
+ if (parentId === 0) {
73
+ // No place of its own: the app layer takes it HIDDEN, and the flip below shows it — a show()
74
+ // is a display flip wherever the widget is, so its pose plays (a modal's fade) exactly as on
75
+ // a placed one. Appended shown, it would merely appear: a widget shows with its place, no pose.
76
+ this.style({ display: "none" } as any)
77
+ UIOverlay.append(this as any) // pins it: _pinned reads the display
78
78
  } else {
79
- tree().showWidget(this._h.id, TREE_DEST_NONE, 0)
79
+ // Shown = on top of its root: a show() moves the widget to the TAIL of its parent's children
80
+ // (child order is the one ordering rule), so a modal opened later covers an earlier one and
81
+ // the HUD declared after it — while still hidden, so the move is silent (no onClose / onOpen).
82
+ // A state block flipping the display moves nothing: the order is what the tree says.
83
+ const siblings = tree().children(parentId)
84
+ if (siblings[siblings.length - 1] !== this._h.id) {
85
+ tree().remove(parentId, this._h.id)
86
+ tree().insert(parentId, this._h.id, siblings.length - 1)
87
+ }
80
88
  }
89
+ this.style({ display: "flex" } as any)
90
+ this._setShown(true)
81
91
  }
82
92
  hide(): void {
83
93
  if (this._h.id === 0 || !this._shown) return
84
- this._shown = false
85
- tree().hideWidget(this._h.id)
86
- unpin(this._h.id)
94
+ this._setShown(false)
95
+ this.style({ display: "none" } as any)
87
96
  }
88
97
  get isShown(): boolean {
89
98
  return this._shown
90
99
  }
100
+ /** @internal The intent changed (the app's call, or the runtime's report of a cascade flip):
101
+ * subclasses hook the change (UIModal's onOpen / onClose). */
102
+ _setShown(shown: boolean): void {
103
+ if (shown === this._shown) return
104
+ this._shown = shown
105
+ this._shownChanged(shown)
106
+ }
107
+ /** @internal */
108
+ _shownChanged(_shown: boolean): void {}
109
+ /** @internal The runtime: the widget's resolved display flipped — or, at its place, the cascade
110
+ * says otherwise than the style it read in `_pinned` (the runtime reports nothing else there). */
111
+ _emitVisible(shown: boolean): void { this._setShown(shown) }
112
+ /** @internal Placed (inserted under a screen, a layer): shown by its own display from here on. */
113
+ _pinned(): void { this._setShown((this._style as { display?: string }).display !== "none") }
114
+ /** @internal Taken out of its place: shown nowhere. */
115
+ _unpinned(): void { this._setShown(false) }
91
116
 
92
117
  readonly touchStartListeners: any[] = []
93
118
  onTouchStart (callback: any): this {
@@ -138,7 +163,8 @@ const makeWidget = (style: any, children: any) => new WidgetElement("widget", st
138
163
  /** @internal Compiler fast path (chisel `flatten_ui` raw lowering) — see UIContainer. */
139
164
  export const __UIWidget = (children: UINodeChild[] | ChildrenFn): UIWidget => makeWidget({}, children)
140
165
 
141
- /** Create a widget (same argument forms as `UIColumn`). Position it with absolute-style props
166
+ /** Create a widget (same argument forms as `UIColumn`). Put it where it belongs — among a screen's
167
+ * children, in `scene.setContent([...])`, or `UIOverlay` — position it with absolute-style props
142
168
  * (`top`/`left`/`bottom`/`right`), then `show()`/`hide()`. */
143
169
  export function UIWidget(...children: UIChildArg[]): UIWidget;
144
170
  export function UIWidget(children: UINodeChild[] | ChildrenFn): UIWidget;
@@ -2,7 +2,7 @@
2
2
  // Anything that can occupy the app's main slot implements it: UIScreen, Scene (+ARScene),
3
3
  // Scene2D, NativeView, UIVideo. Exactly one Presentable is visible at a time — a screen no
4
4
  // longer overlays a scene, it replaces it (with a transition); UI over a scene goes through
5
- // `UIWidget.attachTo(scene)`.
5
+ // `scene.setContent([hud])` — a widget is the destination's content (docs/plans/widgets-plan.md).
6
6
  //
7
7
  // A destination is addressed by (kind, id): a screen / video by its node id, a scene by its
8
8
  // scene id, a native view by its viewId (`_dest()`). The runtime reports the lifecycle back through
@@ -12,6 +12,7 @@
12
12
  // element code into engine-only bundles.
13
13
 
14
14
  import type { EasingInput } from "../animate/tween/easing"
15
+ import type { UIWidget } from "./UIWidget"
15
16
 
16
17
  /** The built-in transitions (ui/transitions.ts holds what each one is). `push` / `pop` are the
17
18
  * stacked-navigation pair — the incoming screen slides over the other one, which drifts and dims;
@@ -32,6 +33,9 @@ export type TransitionPose = {
32
33
  opacity?: number | number[]
33
34
  /** Black over this screen, 0..1: how dark the screen UNDER the other one gets. */
34
35
  dim?: number | number[]
36
+ /** A widget's overlay layer (its scrim) in the pose — a widget's enter / exit pose only
37
+ * (`UIModal.transition`): `"transparent"` fades the scrim in with the dialog. */
38
+ overlayColor?: string | string[]
35
39
  /** ms (default 300). */
36
40
  duration?: number
37
41
  /** ms to wait; the screen holds its first pose through it. */
@@ -97,6 +101,12 @@ export interface Presentable {
97
101
  onOpen(callback: () => void): this
98
102
  onClose(callback: () => void): this
99
103
  onBack(callback: () => void): this
104
+ /** The destination's widgets (docs/plans/widgets-plan.md): a dialog, a HUD, a sheet declared as
105
+ * CONTENT of the place they belong to. A `UIScreen` takes them among its children; a surface — a
106
+ * `Scene`, a `Scene2D`, a `NativeView`, a `UIVideo` — takes widgets alone, laid out over it while
107
+ * it is presented (touches outside a widget's box reach the surface). A widget shows with its
108
+ * destination unless its style says `display: "none"`; `show()` / `hide()` flip that. */
109
+ setContent(content: (UIWidget | null | undefined | false)[] | (() => (UIWidget | null | undefined | false)[])): this
100
110
  /** @internal Async work that must finish BEFORE this destination replaces the current one
101
111
  * (camera permission, warm render, session launch — ARScene). The previous destination —
102
112
  * typically a loading screen — stays visible while it runs; `open()` and `Router.push/replace`
@@ -9,7 +9,7 @@
9
9
 
10
10
  import type { Easing } from "../animate/easings"
11
11
  import { DEFAULT_EASING, parseEasing, type EasingSpec } from "../animate/tween/easing"
12
- import { CLOCK_UI, DOM_UI, KIND_FLOAT, KIND_TRANSFORM, buildBlob, parseTransformList, transformLanes, warnOnce, type Keyframe, type Track, type TweenTarget } from "../animate/tween/spec"
12
+ import { CLOCK_UI, DOM_UI, KIND_FLOAT, KIND_STRING, KIND_TRANSFORM, buildBlob, parseTransformList, transformLanes, warnOnce, type Keyframe, type Track, type TweenTarget } from "../animate/tween/spec"
13
13
  import type { Transition, TransitionName, TransitionPose, TransitionSpec } from "./presentable"
14
14
 
15
15
  /** Wire ids: a registered transition is > 0. */
@@ -120,7 +120,9 @@ export const mirrored = (s: TransitionSpec): TransitionSpec => ({
120
120
 
121
121
  /** One prop of a side: its keys in the order the side plays them (an incoming side ends at rest
122
122
  * after them, an outgoing one starts at rest before them), and what rest is. */
123
- type Lane = { prop: string, kind: number, sig?: string, keys: number[][], rest: number[] }
123
+ // A string lane (a widget's `overlayColor`): its keys are strings, its rest is CAPTURED at play —
124
+ // the widget's own scrim color, which the transition cannot know.
125
+ type Lane = { prop: string, kind: number, sig?: string, keys: number[][], rest: number[], strKeys?: string[] }
124
126
  type Side = { lanes: Lane[], atMs: number, durMs: number, easing: EasingSpec }
125
127
 
126
128
  const list = <T>(v: T | T[]): T[] => (Array.isArray(v) ? v : [v])
@@ -141,12 +143,16 @@ const transformLane = (value: string | string[]): Lane | null => {
141
143
  const floatLane = (prop: string, value: number | number[], rest: number): Lane =>
142
144
  ({ prop, kind: KIND_FLOAT, keys: list(value).map((v) => [v]), rest: [rest] })
143
145
 
146
+ const stringLane = (prop: string, value: string | string[]): Lane =>
147
+ ({ prop, kind: KIND_STRING, keys: list(value).map(() => [0]), rest: [0], strKeys: list(value) })
148
+
144
149
  const sideOf = (pose: TransitionPose | undefined): Side | null => {
145
150
  if (!pose) return null
146
151
  const lanes: Lane[] = []
147
152
  if (pose.transform !== undefined) { const l = transformLane(pose.transform); if (l) lanes.push(l) }
148
153
  if (pose.opacity !== undefined) lanes.push(floatLane("opacity", pose.opacity, 1))
149
154
  if (pose.dim !== undefined) lanes.push(floatLane("dim", pose.dim, 0))
155
+ if (pose.overlayColor !== undefined) lanes.push(stringLane("overlayColor", pose.overlayColor))
150
156
  if (lanes.length === 0) return null
151
157
  return {
152
158
  lanes,
@@ -162,7 +168,7 @@ const sideOf = (pose: TransitionPose | undefined): Side | null => {
162
168
  * the direction being the opposite one. No dim: nothing is over it. */
163
169
  const borrowed = (side: Side | null): Side | null => {
164
170
  if (!side) return null
165
- const lanes = side.lanes.filter((l) => l.prop !== "dim").map((l) => {
171
+ const lanes = side.lanes.filter((l) => l.prop !== "dim" && l.strKeys === undefined).map((l) => {
166
172
  if (l.kind !== KIND_TRANSFORM) return { ...l, keys: l.keys.slice().reverse() }
167
173
  const sign: number[] = []
168
174
  for (const fn of l.sig!.split(",")) for (let i = transformLanes(fn); i > 0; i--) sign.push(fn.startsWith("translate") ? -1 : 1)
@@ -178,6 +184,12 @@ const tracksOf = (side: Side | null, role: number, incoming: boolean): Track[] =
178
184
  if (!side) return []
179
185
  const channel = { domain: DOM_UI, id: () => role + 1, value: () => null }
180
186
  return side.lanes.map((l) => {
187
+ if (l.strKeys !== undefined) {
188
+ // a string lane: the keys as strings, the rest captured at play (the node's own value)
189
+ const values: (string | null)[] = incoming ? [...l.strKeys, null] : [null, ...l.strKeys]
190
+ const keys: Keyframe[] = values.map((str, i) => ({ t: i / (values.length - 1), easing: side.easing, value: str === null ? undefined : { str } }))
191
+ return { target: roleTarget, prop: l.prop, channel, kind: l.kind, lanes: 1, atMs: side.atMs, durMs: side.durMs, commit: false, keys }
192
+ }
181
193
  // explicit keys at both ends: a captured one that fails to read would leave a screen in its pose
182
194
  const values = incoming ? [...l.keys, l.rest] : [l.rest, ...l.keys]
183
195
  const keys: Keyframe[] = values.map((lanes, i) => ({ t: i / (values.length - 1), easing: side.easing, value: { lanes } }))
@@ -186,15 +198,16 @@ const tracksOf = (side: Side | null, role: number, incoming: boolean): Track[] =
186
198
  }
187
199
 
188
200
  /** @internal The blob of a spec: `roles[i]` is the role of the blob's target `i`. Null = the spec
189
- * moves nothing. */
190
- export const _transitionBlob = (spec: TransitionSpec): { data: Float32Array, strings: string[], roles: number[], flags: number } | null => {
201
+ * moves nothing. `ownAlone`: the ALONE roles are the spec's own sides as written (a widget's pose:
202
+ * it enters from `enter`, leaves to `exit`, nothing is borrowed from the other side). */
203
+ export const _transitionBlob = (spec: TransitionSpec, ownAlone = false): { data: Float32Array, strings: string[], roles: number[], flags: number } | null => {
191
204
  const enter = sideOf(spec.enter), exit = sideOf(spec.exit)
192
205
  const enterOnTop = (spec.onTop ?? "enter") === "enter"
193
206
  const tracks = [
194
207
  ...tracksOf(enter, ROLE_ENTER, true),
195
208
  ...tracksOf(exit, ROLE_EXIT, false),
196
- ...tracksOf(enterOnTop ? enter : borrowed(exit), ROLE_ENTER_ALONE, true),
197
- ...tracksOf(enterOnTop ? borrowed(enter) : exit, ROLE_EXIT_ALONE, false),
209
+ ...tracksOf(ownAlone ? enter : enterOnTop ? enter : borrowed(exit), ROLE_ENTER_ALONE, true),
210
+ ...tracksOf(ownAlone ? exit : enterOnTop ? borrowed(enter) : exit, ROLE_EXIT_ALONE, false),
198
211
  ]
199
212
  if (tracks.length === 0) return null
200
213
  let durationMs = 0
@@ -242,8 +255,24 @@ export const _transitionBack = (t: Transition): number => {
242
255
  return e.back
243
256
  }
244
257
 
258
+ /** @internal The wire id of a WIDGET's pose (docs/plans/widgets-plan.md decision 11): the pose it
259
+ * enters from and leaves to, as one transition whose ALONE roles the runtime plays on the widget's
260
+ * display flips. Registered once per distinct pose object; `null` = no pose (TRANSITION_NONE). */
261
+ let poseIds = new WeakMap<TransitionPose, number>()
262
+ export const _widgetPoseId = (pose: TransitionPose | null): number => {
263
+ if (!pose) return TRANSITION_NONE
264
+ let id = poseIds.get(pose)
265
+ if (id === undefined) {
266
+ const blob = _transitionBlob({ enter: pose, exit: pose }, true)
267
+ id = blob ? _creatorTree.defineTransition(blob.data, blob.strings, blob.roles, blob.flags) : TRANSITION_NONE
268
+ poseIds.set(pose, id)
269
+ }
270
+ return id
271
+ }
272
+
245
273
  /** @internal Tests: a new fake runtime has no transition of the previous one. */
246
274
  export const _resetTransitions = (): void => {
247
275
  byName = new Map()
248
276
  bySpec = new WeakMap()
277
+ poseIds = new WeakMap()
249
278
  }
package/src/ui/tree.ts CHANGED
@@ -45,6 +45,7 @@ export const TREE_EVENT_HOVER_ENTER = 32
45
45
  export const TREE_EVENT_HOVER_MOVE = 33
46
46
  export const TREE_EVENT_HOVER_END = 34
47
47
  export const TREE_EVENT_HOVER_CANCEL = 35
48
+ export const TREE_EVENT_WIDGET_VISIBLE = 36
48
49
 
49
50
  // ---- setFlags(id, mask) ---------------------------------------------------------------------------
50
51
  export const TREE_FLAG_INTERACTIVE = 1 << 0
@@ -132,6 +133,8 @@ export interface TreeElement {
132
133
  _pinned?(): void
133
134
  _unpinned?(): void
134
135
  _emitBack?(): boolean
136
+ /** A widget: its resolved display flipped (a style write or the cascade) — `isShown` follows. */
137
+ _emitVisible?(shown: boolean): void
135
138
  }
136
139
 
137
140
  /** @internal A track handler (runtime/touch.ts TrackHandler), as the emitter reads it. */
@@ -285,6 +288,7 @@ const emit = (id: number, kind: number, a?: any, b?: any, c?: any, d?: any, e?:
285
288
  t?.onEnd?.({ clientX: a, clientY: b, deltaX: c, deltaY: d })
286
289
  return
287
290
  }
291
+ case TREE_EVENT_WIDGET_VISIBLE: pins.get(id)?._emitVisible?.(!!a); return
288
292
  case TREE_EVENT_HOVER_CANCEL: {
289
293
  const t = hoverTracker
290
294
  hoverTracker = null
package/src/version.ts CHANGED
@@ -4,7 +4,7 @@
4
4
  // same manifest). Its MAJOR is the bundle ↔ runtime contract: a bundle's `// sdk:` header line
5
5
  // and a host's embedded version must agree on the major for the host to run the bundle; minor
6
6
  // and patch never gate anything (features are detected, never versioned).
7
- export const SDK_VERSION = "2.0.7"
7
+ export const SDK_VERSION = "2.0.9"
8
8
 
9
9
  /** The major of a semver string, or null when it is not one. */
10
10
  export const sdkMajor = (version: string | null | undefined): number | null => {
@@ -9,6 +9,7 @@ import {
9
9
  TREE_EVENT_CLOSE, TREE_EVENT_DETACHED, TREE_EVENT_FREED, TREE_EVENT_OPEN, TREE_EVENT_ROUTER_CHANGE, TREE_EVENT_VLIST_SYNC,
10
10
  } from "../../src/ui/tree"
11
11
  import { _resetTransitions } from "../../src/ui/transitions"
12
+ import { _resetOverlay } from "../../src/ui/UILayer"
12
13
 
13
14
  export type FakeNode = {
14
15
  id: number, type: string, parent: number, children: number[],
@@ -28,7 +29,9 @@ export class FakeTree {
28
29
  displaySize: [number, number] = [0, 0]
29
30
  current: Dest | null = null
30
31
  stack: Dest[] = []
31
- widgets: number[] = []
32
+ /** The layers by destination key (`kind:id`; `255:0` = the app layer) and the widgets' poses. */
33
+ layers = new Map<string, number>()
34
+ poses = new Map<number, number>()
32
35
  freed: number[] = []
33
36
  tweens: { id: number, data: Float32Array, strings: string[], targets: number[] }[] = []
34
37
  tweenEvent: ((id: number, kind: number, index: number, ...lanes: number[]) => void) | null = null
@@ -207,8 +210,8 @@ export class FakeTree {
207
210
  else onReject(`no view ${viewId}`)
208
211
  }
209
212
  viewCallHandler: ((viewId: number, method: string, args: any[], res: (r?: unknown) => void, rej: (m: string) => void) => void) | null = null
210
- showWidget = (id: number, ownerKind: number, ownerId: number): void => { this.rec("showWidget", id, ownerKind, ownerId); if (!this.widgets.includes(id)) this.widgets.push(id) }
211
- hideWidget = (id: number): void => { this.rec("hideWidget", id); this.widgets = this.widgets.filter((w) => w !== id) }
213
+ setLayer = (kind: number, id: number, layer: number): void => { this.rec("setLayer", kind, id, layer); if (layer) this.layers.set(`${kind}:${id}`, layer); else this.layers.delete(`${kind}:${id}`) }
214
+ setWidgetPose = (id: number, transition: number): void => { this.rec("setWidgetPose", id, transition); this.poses.set(id, transition) }
212
215
 
213
216
  // ---- pager -----------------------------------------------------------------------------------------
214
217
  private pagerTop(p: FakeNode): number | null {
@@ -341,6 +344,7 @@ export const installFakeTree = (): FakeTree => {
341
344
  fake.emitter = FakeTree.primary ?? prev?.emitter ?? null
342
345
  if (prev?.tweenEvent) fake.tweenEvent = prev.tweenEvent // registered once per world too
343
346
  _resetTransitions() // …while a transition is registered per RUNTIME: the new fake has none
347
+ _resetOverlay() // …and the app layer is a node of the previous fake
344
348
  ;(globalThis as any)._creatorTree = fake
345
349
  return fake
346
350
  }