lecodes-sdk 2.2.3 → 2.2.5

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 (61) hide show
  1. package/dist/global.d.ts +3 -1
  2. package/dist/types/animate/tween/spec.d.ts +4 -5
  3. package/dist/types/g2/Scene2D.d.ts +5 -4
  4. package/dist/types/gl/Scene.d.ts +5 -4
  5. package/dist/types/inject.d.ts +1 -1
  6. package/dist/types/server/db/fields.d.ts +6 -3
  7. package/dist/types/server/db/marci/query.d.ts +21 -2
  8. package/dist/types/server/db/types.d.ts +26 -12
  9. package/dist/types/server/errors.d.ts +4 -1
  10. package/dist/types/server/files/models.d.ts +4 -0
  11. package/dist/types/server/inject.d.ts +6 -0
  12. package/dist/types/ui/NativeView.d.ts +3 -3
  13. package/dist/types/ui/UI.d.ts +1 -1
  14. package/dist/types/ui/UILayer.d.ts +25 -18
  15. package/dist/types/ui/UINode.d.ts +8 -1
  16. package/dist/types/ui/UIScreen.d.ts +11 -1
  17. package/dist/types/ui/UIVideo.d.ts +3 -3
  18. package/dist/types/ui/UIVirtualizedList.d.ts +64 -18
  19. package/dist/types/ui/UIWidget.d.ts +24 -15
  20. package/dist/types/ui/presentable.d.ts +7 -7
  21. package/dist/types/version.d.ts +1 -1
  22. package/dist/types.json +1 -1
  23. package/package.json +2 -2
  24. package/prompts/3d-scene-files.md +2 -2
  25. package/prompts/3d-scene.md +1 -1
  26. package/prompts/dist/2d-game.md +13 -13
  27. package/prompts/dist/3d-app.md +21 -18
  28. package/prompts/dist/ar-app.md +13 -13
  29. package/prompts/dist/ui-app.md +13 -13
  30. package/prompts/ui.md +3 -1
  31. package/src/animate/tween/spec.ts +8 -13
  32. package/src/chisel.ts +3 -3
  33. package/src/compile/serverSplit.ts +5 -3
  34. package/src/g2/Scene2D.ts +9 -7
  35. package/src/gl/Scene.ts +9 -7
  36. package/src/inject.ts +1 -1
  37. package/src/server/db/defineDb.ts +17 -21
  38. package/src/server/db/fields.ts +7 -4
  39. package/src/server/db/httpTransport.ts +19 -3
  40. package/src/server/db/marci/query.ts +34 -8
  41. package/src/server/db/types.ts +33 -14
  42. package/src/server/errors.ts +4 -1
  43. package/src/server/files/db.ts +26 -7
  44. package/src/server/files/host.ts +45 -17
  45. package/src/server/files/models.ts +4 -0
  46. package/src/server/host.ts +3 -3
  47. package/src/server/inject.ts +3 -0
  48. package/src/server/runtime.ts +65 -4
  49. package/src/ui/NativeView.ts +11 -9
  50. package/src/ui/UI.ts +1 -1
  51. package/src/ui/UILayer.ts +90 -42
  52. package/src/ui/UINode.ts +33 -11
  53. package/src/ui/UIPopover.ts +3 -3
  54. package/src/ui/UIScreen.ts +50 -2
  55. package/src/ui/UIVideo.ts +11 -9
  56. package/src/ui/UIVirtualizedList.ts +159 -31
  57. package/src/ui/UIWidget.ts +41 -18
  58. package/src/ui/presentable.ts +8 -8
  59. package/src/ui/transitions.ts +1 -1
  60. package/src/version.ts +1 -1
  61. package/tests/helpers/fakeTree.ts +6 -1
@@ -10,20 +10,14 @@ export type UIVirtualizedListStyle = DrawableStyle & ElementStyle & {
10
10
  keyboardDismissMode?: "interactive" | "scroll" | "none",
11
11
  }
12
12
 
13
- export interface VListConfig<T> {
14
- /** The rows, read reactively: the list follows the signals this reads, the way a container's
15
- * function children do. Rows are told apart by `keyOf`; a row whose item is another object
16
- * re-renders. Appended rows and rows added at the start are told from a rewrite (a chat at
17
- * its bottom follows what is appended; older history above moves nothing). */
18
- data: () => T[]
13
+ interface VListBaseConfig<T> {
19
14
  /** Stable unique key of an item. */
20
15
  keyOf: (item: T) => string
21
16
  /** Subtree factory — called when the item enters the window; the subtree lives until it
22
17
  * leaves. */
23
18
  render: (item: T) => UINodeChild
24
19
  /** Height estimate (px) used before the first measurement — a number, or a per-item function
25
- * (chat: `msg => 40 + Math.ceil(msg.text.length / 35) * 20`). Computed once when the item is
26
- * added. */
20
+ * (chat: `msg => 40 + Math.ceil(msg.text.length / 35) * 20`). Read when the item is added. */
27
21
  estimatedHeight: number | ((item: T) => number)
28
22
  /** Extra px around the viewport kept mounted. Default: one viewport height. */
29
23
  overscan?: number
@@ -32,9 +26,26 @@ export interface VListConfig<T> {
32
26
  inverted?: boolean
33
27
  }
34
28
 
35
- /** A windowed (virtualized) list: only the items near the viewport are mounted — for long feeds
36
- * and chats. Fed by `data`, a function over signals. */
37
- export interface UIVirtualizedList<T = unknown> extends UIElementBase<UIVirtualizedListStyle> {
29
+ /** A list fed by `data`: the rows are read reactively, the way a container's function children
30
+ * are. Such a list has no imperative feed (`append` and the rest are not on its type). */
31
+ export interface VListBoundConfig<T> extends VListBaseConfig<T> {
32
+ /** The rows, read reactively: the list follows the signals this reads. Rows are told apart by
33
+ * `keyOf`; a row whose item is another object re-renders. The change is told from the keys:
34
+ * rows gone are removals, new rows are inserts where they stand (a chat at its bottom follows
35
+ * an append; rows above the reader move nothing), a reorder rewrites the keys. */
36
+ data: () => T[]
37
+ }
38
+
39
+ /** A list fed by hand: `setData` / `append` / `prepend` / `update` / `removeByKey`. */
40
+ export interface VListFedConfig<T> extends VListBaseConfig<T> {
41
+ data?: undefined
42
+ }
43
+
44
+ export type VListConfig<T> = VListBoundConfig<T> | VListFedConfig<T>
45
+
46
+ /** What both forms of the list share: the items held, scrolling, the scroll / refresh / edge
47
+ * callbacks. */
48
+ export interface UIVirtualizedListBase<T = unknown> extends UIElementBase<UIVirtualizedListStyle> {
38
49
  readonly type: "vlist"
39
50
 
40
51
  /** Number of data items (mounted or not). */
@@ -60,7 +71,31 @@ export interface UIVirtualizedList<T = unknown> extends UIElementBase<UIVirtuali
60
71
  onStartReached(thresholdPx: number, callback: () => void): this
61
72
  }
62
73
 
63
- export class VListElement<T> extends Element<"vlist"> implements UIVirtualizedList<T> {
74
+ /** A virtualized list fed by `data` (a function over signals): write the signal, the list follows. */
75
+ export interface UIBoundVirtualizedList<T = unknown> extends UIVirtualizedListBase<T> {}
76
+
77
+ /** A virtualized list fed by hand. Each call goes straight to the runtime: `append` is O(1). */
78
+ export interface UIFedVirtualizedList<T = unknown> extends UIVirtualizedListBase<T> {
79
+ /** Replace the data wholesale — diffed by key like a `data` write (vanished keys are removed,
80
+ * new ones inserted where they stand, a reorder rewrites). */
81
+ setData(items: T[]): this
82
+ /** Append items to the end (auto-scrolls when `inverted` and resting at the bottom). */
83
+ append(...items: T[]): this
84
+ /** Insert at the start WITHOUT a scroll jump (the runtime compensates the offset). */
85
+ prepend(...items: T[]): this
86
+ /** Update items with the same keys: mounted ones re-render; the cached height persists until
87
+ * re-measured. */
88
+ update(...items: T[]): this
89
+ /** Remove items by key. */
90
+ removeByKey(...keys: string[]): this
91
+ }
92
+
93
+ /** A windowed (virtualized) list: only the items near the viewport are mounted — for long feeds
94
+ * and chats. Fed by `data`, a function over signals, or by hand (`append` and the rest); which
95
+ * one is the config's and the type's. */
96
+ export type UIVirtualizedList<T = unknown> = UIBoundVirtualizedList<T> | UIFedVirtualizedList<T>
97
+
98
+ export class VListElement<T> extends Element<"vlist"> implements UIFedVirtualizedList<T> {
64
99
  private readonly sl: ((scrollPosition: number) => void)[] = [] // scroll
65
100
  private readonly erl: (() => void)[] = [] // endReached
66
101
  private readonly strl: (() => void)[] = [] // startReached
@@ -74,6 +109,7 @@ export class VListElement<T> extends Element<"vlist"> implements UIVirtualizedLi
74
109
  private estimate: (item: T) => number
75
110
  private readonly _overscan: number
76
111
  private readonly _inverted: boolean
112
+ private readonly _bound: boolean
77
113
  private _endThreshold = 0
78
114
  private _startThreshold = 0
79
115
  private _inited = false
@@ -90,10 +126,12 @@ export class VListElement<T> extends Element<"vlist"> implements UIVirtualizedLi
90
126
  : () => config.estimatedHeight as number
91
127
  this._overscan = config.overscan ?? -1
92
128
  this._inverted = config.inverted ?? false
129
+ const data = config.data
130
+ this._bound = data !== undefined
93
131
  // The data binding is the element's (it goes with it); its first run waits a microtask so the
94
132
  // edge thresholds chained after the constructor (`.onEndReached(…)`) are in the runtime's list
95
133
  // state, which is created on the first write.
96
- Promise.resolve().then(() => { if (this._h.id !== 0) createBinding(this, "data", () => this._apply(config.data())) })
134
+ if (data !== undefined) Promise.resolve().then(() => { if (this._h.id !== 0) createBinding(this, "data", () => this._apply(data())) })
97
135
  }
98
136
 
99
137
  // The runtime's list state is created on the first data write — after the app had its chance to
@@ -104,42 +142,68 @@ export class VListElement<T> extends Element<"vlist"> implements UIVirtualizedLi
104
142
  tree().vlistInit(this._h.id, this._overscan, this._inverted, this._startThreshold, this._endThreshold)
105
143
  }
106
144
 
107
- /** The data as it is now against what the runtime holds: rows added at the end or at the start
108
- * are inserts (the runtime anchors the scroll: a chat follows an append, history above moves
109
- * nothing), rows gone are removals, anything else a rewrite of the keys (measured heights stay). */
145
+ /** The two feeds are exclusive per list: a hand-made change to a list `data` feeds would be
146
+ * undone by the binding's next run, so it is refused and logged. */
147
+ private _fed(method: string): boolean {
148
+ if (!this._bound) return true
149
+ console.error(`UIVirtualizedList.${method}(): the list is fed by data — write the signal it reads instead`)
150
+ return false
151
+ }
152
+
153
+ /** The data as it is now against what the runtime holds. Keys gone are removals, keys new are
154
+ * inserts where they stand in the new order — both anchored by the runtime (a chat follows an
155
+ * append, rows above the reader move nothing) — as long as the keys kept keep their order;
156
+ * a reorder rewrites the keys (measured heights stay). A mounted row whose item is another
157
+ * object re-renders. */
110
158
  private _apply(next: T[]): void {
111
159
  const prevKeys = this.keys
112
160
  const prevItems = this.items
113
161
  const keys: string[] = []
114
- const estimates: number[] = []
115
162
  const items = new Map<string, T>()
116
163
  for (const item of next) {
117
164
  const k = this.keyOf(item)
118
165
  if (items.has(k)) continue // a repeated key: the first row stands
119
166
  items.set(k, item)
120
167
  keys.push(k)
121
- estimates.push(this.estimate(item))
122
168
  }
123
169
  this.keys = keys
124
170
  this.items = items
125
171
  this._ensure()
126
172
  const id = this._h.id
127
173
  if (id === 0) return
174
+ // the estimates of keys[from, to): read for the rows the runtime is told of, not for every row
175
+ const estimates = (from: number, to: number): number[] => {
176
+ const out: number[] = []
177
+ for (let i = from; i < to; i++) out.push(this.estimate(items.get(keys[i])!))
178
+ return out
179
+ }
128
180
  const n = prevKeys.length, m = keys.length
129
- const same = (from: number) => { for (let i = 0; i < n; i++) if (keys[from + i] !== prevKeys[i]) return false; return true }
130
181
  if (n === 0 || m === 0) {
131
- tree().vlistSetKeys(id, keys, estimates)
132
- } else if (m >= n && same(0)) {
133
- if (m > n) tree().vlistInsertKeys(id, -1, keys.slice(n), estimates.slice(n)) // the same keys: nothing to tell
134
- } else if (m > n && same(m - n)) {
135
- tree().vlistInsertKeys(id, 0, keys.slice(0, m - n), estimates.slice(0, m - n))
182
+ tree().vlistSetKeys(id, keys, estimates(0, m))
136
183
  } else {
137
- // removals only: the new keys are the old ones in order, some left out
184
+ // the keys kept, in the old order, must be the keys kept in the new order
138
185
  const gone: string[] = []
139
- let j = 0
140
- for (const k of prevKeys) { if (j < m && keys[j] === k) j++; else gone.push(k) }
141
- if (m < n && j === m) tree().vlistRemoveKeys(id, gone)
142
- else tree().vlistSetKeys(id, keys, estimates)
186
+ let j = 0, ordered = true
187
+ for (const k of prevKeys) {
188
+ if (!items.has(k)) { gone.push(k); continue }
189
+ while (j < m && !prevItems.has(keys[j])) j++ // skip the new rows
190
+ if (keys[j] !== k) { ordered = false; break }
191
+ j++
192
+ }
193
+ if (!ordered) {
194
+ tree().vlistSetKeys(id, keys, estimates(0, m))
195
+ } else {
196
+ if (gone.length > 0) tree().vlistRemoveKeys(id, gone)
197
+ // runs of new rows, in order: everything before a run is already in the list, so the
198
+ // run's position in the new array is its index in the list as it stands
199
+ for (let i = 0; i < m;) {
200
+ if (prevItems.has(keys[i])) { i++; continue }
201
+ let e = i + 1
202
+ while (e < m && !prevItems.has(keys[e])) e++
203
+ tree().vlistInsertKeys(id, e === m ? -1 : i, keys.slice(i, e), estimates(i, e))
204
+ i = e
205
+ }
206
+ }
143
207
  }
144
208
  // a mounted row whose item is another object re-renders; the others pick theirs up when they re-enter
145
209
  for (const k of this.mounted.keys()) {
@@ -147,6 +211,67 @@ export class VListElement<T> extends Element<"vlist"> implements UIVirtualizedLi
147
211
  }
148
212
  }
149
213
 
214
+ /** The new rows of `items` (keys not held yet, repeats dropped), registered. */
215
+ private _register(items: T[]): [string[], number[]] {
216
+ const keys: string[] = []
217
+ const estimates: number[] = []
218
+ for (const item of items) {
219
+ const k = this.keyOf(item)
220
+ if (this.items.has(k)) continue
221
+ this.items.set(k, item)
222
+ keys.push(k)
223
+ estimates.push(this.estimate(item))
224
+ }
225
+ return [keys, estimates]
226
+ }
227
+
228
+ // --- the hand feed: each call is the runtime's operation, no diff ---
229
+ setData(items: T[]): this {
230
+ if (this._fed("setData")) this._apply(items)
231
+ return this
232
+ }
233
+
234
+ append(...items: T[]): this {
235
+ if (!this._fed("append")) return this
236
+ const [keys, estimates] = this._register(items)
237
+ if (keys.length === 0) return this
238
+ this.keys.push(...keys)
239
+ this._ensure()
240
+ if (this._h.id !== 0) tree().vlistInsertKeys(this._h.id, -1, keys, estimates)
241
+ return this
242
+ }
243
+
244
+ prepend(...items: T[]): this {
245
+ if (!this._fed("prepend")) return this
246
+ const [keys, estimates] = this._register(items)
247
+ if (keys.length === 0) return this
248
+ this.keys = keys.concat(this.keys)
249
+ this._ensure()
250
+ if (this._h.id !== 0) tree().vlistInsertKeys(this._h.id, 0, keys, estimates)
251
+ return this
252
+ }
253
+
254
+ update(...items: T[]): this {
255
+ if (!this._fed("update")) return this
256
+ for (const item of items) {
257
+ const k = this.keyOf(item)
258
+ if (!this.items.has(k) || this.items.get(k) === item) continue
259
+ this.items.set(k, item)
260
+ if (this._h.id !== 0 && this.mounted.has(k)) tree().vlistInvalidate(this._h.id, k)
261
+ }
262
+ return this
263
+ }
264
+
265
+ removeByKey(...keys: string[]): this {
266
+ if (!this._fed("removeByKey")) return this
267
+ const gone: string[] = []
268
+ for (const k of keys) if (this.items.delete(k)) gone.push(k)
269
+ if (gone.length === 0) return this
270
+ this.keys = this.keys.filter((k) => this.items.has(k))
271
+ if (this._h.id !== 0) tree().vlistRemoveKeys(this._h.id, gone)
272
+ return this
273
+ }
274
+
150
275
  get itemCount(): number { return this.items.size }
151
276
  getItem(key: string): T | undefined { return this.items.get(key) }
152
277
 
@@ -212,8 +337,11 @@ export class VListElement<T> extends Element<"vlist"> implements UIVirtualizedLi
212
337
  }
213
338
  }
214
339
 
215
- /** Create a virtualized list from a config (`data` + `keyOf` + `render` + `estimatedHeight`
216
- * required). Give it a size (`flexGrow: 1` as a screen body). */
340
+ /** Create a virtualized list (`keyOf` + `render` + `estimatedHeight` required). With `data` the
341
+ * list follows the signals it reads and has no hand feed; without it, feed it with `setData` /
342
+ * `append` / `prepend` / `update` / `removeByKey`. Give it a size (`flexGrow: 1` as a screen body). */
343
+ export function UIVirtualizedList<T>(config: VListBoundConfig<T>): UIBoundVirtualizedList<T>
344
+ export function UIVirtualizedList<T>(config: VListFedConfig<T>): UIFedVirtualizedList<T>
217
345
  export function UIVirtualizedList<T>(config: VListConfig<T>): UIVirtualizedList<T> {
218
346
  return new VListElement<T>(config)
219
347
  }
@@ -1,7 +1,8 @@
1
1
  import { TouchStartEvent } from "../runtime/touch"
2
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"
3
- import { UIOverlay } from "./UILayer"
4
- import { TREE_FLAG_BACK, TREE_FLAG_INTERACTIVE, TREE_FLAG_OVERLAY_TAP, tree } from "./tree"
3
+ import { _appLayer } from "./UILayer"
4
+ import type { Presentable } from "./presentable"
5
+ import { TREE_FLAG_BACK, TREE_FLAG_INTERACTIVE, TREE_FLAG_OVERLAY_TAP, tree, unpin } from "./tree"
5
6
 
6
7
 
7
8
  export type UIWidgetStyle = ContainerStyle & DrawableStyle & PaddingStyle & BaseStyle & PositionStyle & {
@@ -14,13 +15,13 @@ export type UIWidgetStyle = ContainerStyle & DrawableStyle & PaddingStyle & Base
14
15
  }
15
16
 
16
17
 
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.
18
+ /** A floating overlay that belongs to a DESTINATION, not to a box (docs/plans/widgets-plan.md): a
19
+ * dialog, a sheet, a HUD, a mini-player. A widget is one of the `widgets` of the place it belongs
20
+ * to — `UIScreen(content).widgets(dialog)`, `scene.widgets(hud)` — and is laid out and painted in
21
+ * the box of the outermost presented destination that contains it: absolutely positioned in that
22
+ * box, above everything else in it. A widget on a tab page covers the tab bar and rides the
23
+ * shell's push and pop. A widget among the children of a box is an error: a floating element of
24
+ * a box is an absolute child, not a widget.
24
25
  *
25
26
  * It shows with its place unless its style says `display: "none"`; `show()` / `hide()` flip that,
26
27
  * and so does a state block (`$class: { display: "flex" }`). `UIModal` / `UIPopover` /
@@ -28,13 +29,12 @@ export type UIWidgetStyle = ContainerStyle & DrawableStyle & PaddingStyle & Base
28
29
  export interface UIWidget extends UIContainerBase<UIWidgetStyle, DrawableStyle & BaseStyle & { overlayColor?: Color | null }> {
29
30
  readonly type: "widget",
30
31
 
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()`. */
32
+ /** Show the widget: `display: "flex"`, on top of its root — it moves to the end of its
33
+ * destination's widgets, so a dialog opened later covers an earlier one and a HUD declared after
34
+ * it. A widget that belongs nowhere goes ABOVE EVERY destination — a global loader, a dialog
35
+ * opened from anywhere — and stays there, hidden by `hide()`, until it is given a place. */
35
36
  show(): void,
36
- /** Hide the widget: `display: "none"`. It stays where it is (its place, the app layer), ready
37
- * for the next `show()`. */
37
+ /** Hide the widget: `display: "none"`. It stays where it is, ready for the next `show()`. */
38
38
  hide(): void,
39
39
 
40
40
  /** Shown right now by its own display (`show()` ran, `hide()` hasn't, no state block hides
@@ -42,6 +42,14 @@ export interface UIWidget extends UIContainerBase<UIWidgetStyle, DrawableStyle &
42
42
  * visibility, not the widget's. */
43
43
  readonly isShown: boolean,
44
44
 
45
+ /** @deprecated A widget is one of the `widgets` of the place it belongs to (2026-10-07): write
46
+ * `UIScreen(content).widgets(widget)` / `scene.widgets(widget)` instead. This alias is
47
+ * `owner.widgets.append(widget)` with the widget hidden, so the `show()` that follows flips it as
48
+ * on any placed widget; `attachTo(null)` takes it out, and the next `show()` puts it above every
49
+ * destination. For the projects compiled before the change; removed with the release that
50
+ * recompiles them. */
51
+ attachTo(owner: Presentable | null): this,
52
+
45
53
  /** Touch began on the widget; `ev.track(...)` takes over the rest of the gesture. */
46
54
  onTouchStart(callback: (ev: TouchStartEvent<UIWidget>) => void): this,
47
55
  /** Tap on the overlay layer (requires an `overlayColor`). */
@@ -74,7 +82,7 @@ export class WidgetElement extends ContainerElement<"widget"> {
74
82
  // is a display flip wherever the widget is, so its pose plays (a modal's fade) exactly as on
75
83
  // a placed one. Appended shown, it would merely appear: a widget shows with its place, no pose.
76
84
  this.style({ display: "none" } as any)
77
- UIOverlay.append(this as any) // pins it: _pinned reads the display
85
+ _appLayer().append(this as any) // pins it: _pinned reads the display
78
86
  } else {
79
87
  // Shown = on top of its root: a show() moves the widget to the TAIL of its parent's children
80
88
  // (child order is the one ordering rule), so a modal opened later covers an earlier one and
@@ -97,6 +105,21 @@ export class WidgetElement extends ContainerElement<"widget"> {
97
105
  get isShown(): boolean {
98
106
  return this._shown
99
107
  }
108
+ /** @deprecated see the interface */
109
+ attachTo(owner: Presentable | null): this {
110
+ if (this._h.id === 0) return this
111
+ const parentId = tree().parent(this._h.id)
112
+ if (parentId !== 0) { // one parent: a re-attach moves it
113
+ tree().remove(parentId, this._h.id)
114
+ unpin(this._h.id) // as a node's remove(): shown nowhere
115
+ }
116
+ if (owner) {
117
+ // Hidden first: a widget shows with its place, and the old contract showed it on show() only.
118
+ this.style({ display: "none" } as any)
119
+ owner.widgets.append(this as any)
120
+ }
121
+ return this
122
+ }
100
123
  /** @internal The intent changed (the app's call, or the runtime's report of a cascade flip):
101
124
  * subclasses hook the change (UIModal's onOpen / onClose). */
102
125
  _setShown(shown: boolean): void {
@@ -163,8 +186,8 @@ const makeWidget = (style: any, children: any) => new WidgetElement("widget", st
163
186
  /** @internal Compiler fast path (chisel `flatten_ui` raw lowering) — see UIContainer. */
164
187
  export const __UIWidget = (children: UINodeChild[] | ChildrenFn): UIWidget => makeWidget({}, children)
165
188
 
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
189
+ /** Create a widget (same argument forms as `UIColumn`). Give it to the destination it belongs to —
190
+ * `UIScreen(content).widgets(w)`, `scene.widgets(w)` — position it with absolute-style props
168
191
  * (`top`/`left`/`bottom`/`right`), then `show()`/`hide()`. */
169
192
  export function UIWidget(...children: UIChildArg[]): UIWidget;
170
193
  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
- // `scene.setContent([hud])` — a widget is the destination's content (docs/plans/widgets-plan.md).
5
+ // `scene.widgets(hud)` — a widget belongs to a destination (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,7 +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
+ import type { WidgetSet } from "./UILayer"
16
16
 
17
17
  /** The built-in transitions (ui/transitions.ts holds what each one is). `push` / `pop` are the
18
18
  * stacked-navigation pair — the incoming screen slides over the other one, which drifts and dims;
@@ -101,12 +101,12 @@ export interface Presentable {
101
101
  onOpen(callback: () => void): this
102
102
  onClose(callback: () => void): this
103
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
104
+ /** The destination's widgets (docs/plans/widgets-plan.md): a dialog, a HUD, a sheet — what floats
105
+ * over it. `x.widgets(a, b)` sets them, `x.widgets.append` / `.remove` manage them, `x.widgets.list`
106
+ * reads them; the same on a `UIScreen` (apart from its `children`), a `Scene`, a `Scene2D`, a
107
+ * `NativeView`, a `UIVideo`. A widget shows with its destination unless its style says
108
+ * `display: "none"`; `show()` / `hide()` flip that. */
109
+ readonly widgets: WidgetSet<this>
110
110
  /** @internal Async work that must finish BEFORE this destination replaces the current one
111
111
  * (camera permission, warm render, session launch — ARScene). The previous destination —
112
112
  * typically a loading screen — stays visible while it runs; `open()` and `Router.push/replace`
@@ -128,7 +128,7 @@ type Side = { lanes: Lane[], atMs: number, durMs: number, easing: EasingSpec }
128
128
  const list = <T>(v: T | T[]): T[] => (Array.isArray(v) ? v : [v])
129
129
 
130
130
  const transformLane = (value: string | string[]): Lane | null => {
131
- const parsed = list(value).map((v) => parseTransformList(v, true))
131
+ const parsed = list(value).map((v) => parseTransformList(v))
132
132
  const first = parsed[0]
133
133
  if (!first || parsed.some((p) => p === null || p.sig !== first.sig)) {
134
134
  warnOnce("transition:transform", "a transition's transform must be a list of translate / scale / rotate, the same functions in every keyframe — ignored")
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.2.3"
7
+ export const SDK_VERSION = "2.2.5"
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 => {
@@ -87,7 +87,12 @@ export class FakeTree {
87
87
  if (!p || !c) return
88
88
  if (c.parent !== 0) return
89
89
  c.parent = parent
90
- const i = Math.max(0, Math.min(index, p.children.length))
90
+ // tree.cpp treeInsert: a widget is the TAIL of a screen's children (its index counted past the
91
+ // content, clamped into the tail); a content index past the content lands before the widgets.
92
+ const own = p.type === "layer" ? 0 : p.children.filter((x) => this.nodes.get(x)?.type !== "widget").length
93
+ const i = c.type === "widget"
94
+ ? own + Math.max(0, Math.min(index < own ? 0 : index - own, p.children.length - own))
95
+ : Math.max(0, Math.min(index, own))
91
96
  p.children.splice(i, 0, child)
92
97
  }
93
98
  remove = (parent: number, child: number): void => {