lecodes-sdk 2.2.4 → 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.
package/package.json CHANGED
@@ -45,7 +45,7 @@
45
45
  "marcidb-embedded": "^0.15.0",
46
46
  "sharp": "^0.35.2"
47
47
  },
48
- "version": "2.2.4",
48
+ "version": "2.2.5",
49
49
  "files": [
50
50
  "src",
51
51
  "dist",
@@ -69,7 +69,7 @@ export class Spinner extends Aspect<'spinner'> {
69
69
  // a UIWidget that is the scene's CONTENT — it shows and hides together with it:
70
70
  const hud = UIWidget(UIText('Score: 0').style({ color: 'white', fontSize: 24 }))
71
71
  .style({ top: 'max(safe-top, 16px)', left: 16 })
72
- scene.setContent([hud])
72
+ scene.widgets(hud)
73
73
 
74
74
  // ===== SCENE FILE MISTAKES — DO NOT DO THESE =====
75
75
 
@@ -107,7 +107,7 @@ const { scene, nodes } = await mainScene.open()
107
107
 
108
108
  let taps = 0
109
109
  const label = UIText('Tap the crate').style({ color: 'white', fontSize: 20, fontWeight: 700 })
110
- scene.setContent([UIWidget(label).style({ top: 'max(safe-top, 16px)', left: 16 })])
110
+ scene.widgets(UIWidget(label).style({ top: 'max(safe-top, 16px)', left: 16 }))
111
111
 
112
112
  nodes.crate.addEventListener('click', () => { label.text = `Taps: ${++taps}` })
113
113
  </file>
@@ -117,6 +117,6 @@ const hud = UIWidget(
117
117
  scoreLabel = UIText('Score: 0').style({ color: 'white', fontSize: 24, fontWeight: 700 }),
118
118
  ).style({ top: 'max(safe-top, 16px)', right: 16 })
119
119
 
120
- scene.setContent([hud])
120
+ scene.widgets(hud)
121
121
  scene.open()
122
122
  </file>
@@ -112,7 +112,7 @@ Everything in this prompt is a global — no imports needed. A project's only im
112
112
 
113
113
  Imports of the project's own files are also managed automatically: if your <edit> or <replace> makes code reference another file's export, the import is added for you — never fall back to a whole <file> rewrite just to change import lines. When writing a complete <file>, include imports normally.
114
114
 
115
- Reference a project asset (image, font, video, .svg, .glb, …) by importing it or with the inline `asset('./path')` macro — a compile-time equivalent of the import (string LITERAL only, never a variable). External `https://…` URLs are used directly as strings.
115
+ Reference a project asset (image, font, video, .svg, .glb, …) by its path as a string LITERAL — `UIImage("images/hero.png")`, `"./x.png"` from the file, `"/x.png"` from the root — or with the inline `asset('./path')` macro; both are a compile-time equivalent of the import (a literal, never a variable; a path that names no file fails the compile). A `.json` path stays a string. External `https://…` URLs are used directly as strings.
116
116
 
117
117
  ## Project context
118
118
 
@@ -229,6 +229,7 @@ new WebSocket(url, headers?: Record<string, string>)
229
229
  // ===== STORAGE / DEVICE / MISC =====
230
230
 
231
231
  localStorage.getItem(key): string | null / .setItem(key, value) / .removeItem(key) // nothing else
232
+ signal<Line[]>([]).stored("cart") // a signal kept on the device under the key (JSON values): back on the next launch
232
233
 
233
234
  device.platform: "ios" | "android" | "web" // getter, no ()
234
235
  device.language: string // "en", "ru", …
@@ -794,6 +795,8 @@ const tabs = UITabs({
794
795
  Router.init(tabs) // UITabs IS a UIScreen — present it directly
795
796
  // .select(id), .tab (getter), .onSelect(cb(id, i)) — fires on a bar tap, swipe, or select()
796
797
  // .badge(id, value) — true = dot, number/string = count pill, false/null/0 clears
798
+ // Ambient, from any screen, no reference threading: UITabs.current (the visible tabs, or null) and
799
+ // UITabs.select("profile") — the twin of UIPager.current / UIPager.push
797
800
  // .pager — the UIPager underneath; UIPager.push(detail) from any screen keeps the bar
798
801
  // The bar is colored by the theme's roles (accent, textMuted, surface, border) — no keys of its
799
802
  // own; one bar's own colors: tabs.bar.theme({ accent, surface }). Custom layout: UIPager below.
@@ -876,17 +879,17 @@ UIScrollable(...children)
876
879
  // visible rows (plus a buffer) are mounted. Use it instead of UIScrollable + map() whenever the item count
877
880
  // is large, grows over time, or is unknown.
878
881
  UIVirtualizedList<T>({
882
+ data?: () => T[], // the rows, read reactively: write the signal, the list follows (new array, keys matched)
883
+ // absent = fed by hand: .setData(items) / .append(...items) / .prepend(...items) / .update(...items) / .removeByKey(...keys)
884
+ // (one feed per list: a list with `data` has no append on its type)
879
885
  keyOf: (item: T) => string, // STABLE unique id per item (never the array index)
880
886
  render: (item: T) => UINodeChild, // builds one row; called lazily as rows enter the window
881
887
  estimatedHeight: number | ((item: T) => number), // px guess per row (real height measured after mount)
882
888
  overscan?: number, // extra px mounted above/below the viewport (default: one viewport)
883
889
  inverted?: boolean, // true = chat mode: starts scrolled to the end, append at bottom auto-scrolls
884
890
  })
885
- // Data is driven IMPERATIVELY (no diffing) — call these, never re-create the list:
886
- // .setData(items: T[]) // replace everything (diffed by key)
887
- // .append(...items) / .prepend(...items) // variadic; append = newest chat msg; prepend = history, no scroll jump
888
- // .update(...items) // re-render rows with the same keyOf(); unknown keys ignored
889
- // .removeByKey(...keys)
891
+ // Rows added at the end scroll a chat at its bottom; rows added before the first (history) move nothing; a row
892
+ // whose item is a new object re-renders. Never re-create the list.
890
893
  // .itemCount (getter) / .getItem(key)
891
894
  // .scrollTo(offset, animated?) / .scrollToKey(key, animated?) / .scrollToEnd(animated?) / .onScroll(cb(pos))
892
895
  // .onEndReached(thresholdPx: number, cb) // threshold FIRST and required; fires near the BOTTOM — load next page
@@ -914,7 +917,7 @@ UIImage(src) // src: string url | FetchResponse | File | SvgSource | Canvas (se
914
917
  // .setSourceRect(x, y, w, h) — crop to a sub-rect of the source (texture px); frame fills the box, objectFit
915
918
  // ignored. Chainable, safe to call before mount. For atlases/spritesheets — swap the rect to change frame.
916
919
  // Note: tintColor applies only to SVG sources — it replaces ALL fill and stroke colors (monochrome icons only)
917
- // Note: a bare relative path won't resolve — plain strings are https:// URLs; project files via import or asset('./photo.png')
920
+ // Note: a project file by its path as a string — UIImage("images/photo.png") — is the bundled asset (the compile resolves it; a path that names no file fails the compile); plain https:// strings are remote URLs
918
921
 
919
922
  // UIVideo — the on-screen surface only; playback lives entirely on the VideoPlayer (see AUDIO / VIDEO above)
920
923
  UIVideo(player)
@@ -944,6 +947,7 @@ UIInput() / UITextArea()
944
947
  // type: "text"|"password"|"search"|"phone"|"email"|"number"|"decimal"|"url"|"date"|"time"
945
948
  // enterKey: "done"|"go"|"next"|"search"|"send" (UIInput only) — return-key label
946
949
  // maxLength: number; autocapitalize: "none"|"words"|"sentences"|"characters"; autocorrect: boolean
950
+ // autofocus: boolean — takes the focus as its screen opens (a composer, a chat, a search; once, one per screen)
947
951
  // keyboardShrink: boolean (default true) — false: keyboard OVERLAYS the UI, no relayout (chat composers)
948
952
  // keyboardDismiss: boolean (default true) — false: taps never dismiss the keyboard (chat composers)
949
953
  // $focused: { borderColor, ..., duration? } — style while focused (reserved class)
@@ -1219,6 +1223,8 @@ UIImage(heart).style({ width: 24, height: 24, tintColor: "#666" }) // tintColor
1219
1223
  // ===== ICONS (assetIcon) =====
1220
1224
  // Use real icons instead of emoji. assetIcon("pack:name") resolves the icon at COMPILE time and
1221
1225
  // inlines it as an image source (same shape as SvgSource) — no imports, no project files, offline.
1226
+ // It returns an SvgSource — the same type an imported .svg and SvgSource(xml) give — so a component
1227
+ // that takes an icon is typed (icon: SvgSource), never (icon: string).
1222
1228
  UIImage(assetIcon("lucide:bell")).style({ width: 24, height: 24, tintColor: "#8a8f98" })
1223
1229
  // The id must be a string literal; an unknown id is a compile error (with name suggestions).
1224
1230
  // Recolor via the tintColor style or the { color } option (hex literal bakes in; expression tints).
@@ -1273,12 +1279,6 @@ UIRow(isGroup ? button : UIColumn()) // WRONG
1273
1279
  // ✅ null is skipped in children — no phantom element
1274
1280
  UIRow(isGroup ? button : null)
1275
1281
 
1276
- // ❌ pointing UIImage / bgImage at a project file by bare path — it won't resolve to the bundled asset
1277
- UIImage("./photo.png") // WRONG (a plain string works only for remote http(s) URLs)
1278
- // ✅ import the asset, or wrap its path in asset()
1279
- import photo from './photo.png'
1280
- UIImage(photo) // or: UIImage(asset('./photo.png'))
1281
-
1282
1282
  // ❌ callback-first edge callbacks on UIVirtualizedList
1283
1283
  list.onEndReached(loadNextPage) // WRONG — threshold is the first argument, and required
1284
1284
  // ✅ threshold (px from the edge) first
@@ -112,7 +112,7 @@ Everything in this prompt is a global — no imports needed. A project's only im
112
112
 
113
113
  Imports of the project's own files are also managed automatically: if your <edit> or <replace> makes code reference another file's export, the import is added for you — never fall back to a whole <file> rewrite just to change import lines. When writing a complete <file>, include imports normally.
114
114
 
115
- Reference a project asset (image, font, video, .svg, .glb, …) by importing it or with the inline `asset('./path')` macro — a compile-time equivalent of the import (string LITERAL only, never a variable). External `https://…` URLs are used directly as strings.
115
+ Reference a project asset (image, font, video, .svg, .glb, …) by its path as a string LITERAL — `UIImage("images/hero.png")`, `"./x.png"` from the file, `"/x.png"` from the root — or with the inline `asset('./path')` macro; both are a compile-time equivalent of the import (a literal, never a variable; a path that names no file fails the compile). A `.json` path stays a string. External `https://…` URLs are used directly as strings.
116
116
 
117
117
  ## Project context
118
118
 
@@ -229,6 +229,7 @@ new WebSocket(url, headers?: Record<string, string>)
229
229
  // ===== STORAGE / DEVICE / MISC =====
230
230
 
231
231
  localStorage.getItem(key): string | null / .setItem(key, value) / .removeItem(key) // nothing else
232
+ signal<Line[]>([]).stored("cart") // a signal kept on the device under the key (JSON values): back on the next launch
232
233
 
233
234
  device.platform: "ios" | "android" | "web" // getter, no ()
234
235
  device.language: string // "en", "ru", …
@@ -666,8 +667,11 @@ setLoop(() => { mesh.material = Material.lit({ color: next() }) })
666
667
  new Scene(options?: {
667
668
  ibl?: boolean // image-based ambient light; default TRUE — every scene has ambient
668
669
  environmentIntensity?: number // IBL strength; default 20000
669
- bloom?: boolean // default false
670
- bloomIntensity?: number // default 0.2 (only with bloom: true)
670
+ postProcessing?: { // the look: tone mapping, grading, a .cube LUT, bloom, vignette, AO, reflections, DoF
671
+ toneMapping?: 'aces' | 'neutral' | 'linear' | 'filmic', exposure?: number, contrast?: number, saturation?: number,
672
+ lut?: string, bloom?: boolean | { intensity?: number }, vignette?: boolean | { feather?: number },
673
+ ambientOcclusion?: boolean | { radius?: number }, screenSpaceReflections?: boolean, depthOfField?: boolean | { focusDistance?: number, aperture?: number }
674
+ } // live: scene.postProcessing.contrast = 1.1; .apply({ ... }) = the named fields only; .reset(); never assigned
671
675
  skybox?: ColorInput // solid background / clear color
672
676
  antialias?: boolean // 4× MSAA; default false
673
677
  })
@@ -775,7 +779,7 @@ const hud = UIWidget(
775
779
  scoreLabel = UIText('Score: 0').style({ color: 'white', fontSize: 24, fontWeight: 700 }),
776
780
  ).style({ top: 'max(safe-top, 16px)', right: 16 })
777
781
 
778
- scene.setContent([hud])
782
+ scene.widgets(hud)
779
783
  scene.open()
780
784
  </file>
781
785
 
@@ -850,7 +854,7 @@ export class Spinner extends Aspect<'spinner'> {
850
854
  // a UIWidget that is the scene's CONTENT — it shows and hides together with it:
851
855
  const hud = UIWidget(UIText('Score: 0').style({ color: 'white', fontSize: 24 }))
852
856
  .style({ top: 'max(safe-top, 16px)', left: 16 })
853
- scene.setContent([hud])
857
+ scene.widgets(hud)
854
858
 
855
859
  // ===== SCENE FILE MISTAKES — DO NOT DO THESE =====
856
860
 
@@ -888,7 +892,7 @@ const { scene, nodes } = await mainScene.open()
888
892
 
889
893
  let taps = 0
890
894
  const label = UIText('Tap the crate').style({ color: 'white', fontSize: 20, fontWeight: 700 })
891
- scene.setContent([UIWidget(label).style({ top: 'max(safe-top, 16px)', left: 16 })])
895
+ scene.widgets(UIWidget(label).style({ top: 'max(safe-top, 16px)', left: 16 }))
892
896
 
893
897
  nodes.crate.addEventListener('click', () => { label.text = `Taps: ${++taps}` })
894
898
  </file>
@@ -936,6 +940,8 @@ const tabs = UITabs({
936
940
  Router.init(tabs) // UITabs IS a UIScreen — present it directly
937
941
  // .select(id), .tab (getter), .onSelect(cb(id, i)) — fires on a bar tap, swipe, or select()
938
942
  // .badge(id, value) — true = dot, number/string = count pill, false/null/0 clears
943
+ // Ambient, from any screen, no reference threading: UITabs.current (the visible tabs, or null) and
944
+ // UITabs.select("profile") — the twin of UIPager.current / UIPager.push
939
945
  // .pager — the UIPager underneath; UIPager.push(detail) from any screen keeps the bar
940
946
  // The bar is colored by the theme's roles (accent, textMuted, surface, border) — no keys of its
941
947
  // own; one bar's own colors: tabs.bar.theme({ accent, surface }). Custom layout: UIPager below.
@@ -1018,17 +1024,17 @@ UIScrollable(...children)
1018
1024
  // visible rows (plus a buffer) are mounted. Use it instead of UIScrollable + map() whenever the item count
1019
1025
  // is large, grows over time, or is unknown.
1020
1026
  UIVirtualizedList<T>({
1027
+ data?: () => T[], // the rows, read reactively: write the signal, the list follows (new array, keys matched)
1028
+ // absent = fed by hand: .setData(items) / .append(...items) / .prepend(...items) / .update(...items) / .removeByKey(...keys)
1029
+ // (one feed per list: a list with `data` has no append on its type)
1021
1030
  keyOf: (item: T) => string, // STABLE unique id per item (never the array index)
1022
1031
  render: (item: T) => UINodeChild, // builds one row; called lazily as rows enter the window
1023
1032
  estimatedHeight: number | ((item: T) => number), // px guess per row (real height measured after mount)
1024
1033
  overscan?: number, // extra px mounted above/below the viewport (default: one viewport)
1025
1034
  inverted?: boolean, // true = chat mode: starts scrolled to the end, append at bottom auto-scrolls
1026
1035
  })
1027
- // Data is driven IMPERATIVELY (no diffing) — call these, never re-create the list:
1028
- // .setData(items: T[]) // replace everything (diffed by key)
1029
- // .append(...items) / .prepend(...items) // variadic; append = newest chat msg; prepend = history, no scroll jump
1030
- // .update(...items) // re-render rows with the same keyOf(); unknown keys ignored
1031
- // .removeByKey(...keys)
1036
+ // Rows added at the end scroll a chat at its bottom; rows added before the first (history) move nothing; a row
1037
+ // whose item is a new object re-renders. Never re-create the list.
1032
1038
  // .itemCount (getter) / .getItem(key)
1033
1039
  // .scrollTo(offset, animated?) / .scrollToKey(key, animated?) / .scrollToEnd(animated?) / .onScroll(cb(pos))
1034
1040
  // .onEndReached(thresholdPx: number, cb) // threshold FIRST and required; fires near the BOTTOM — load next page
@@ -1056,7 +1062,7 @@ UIImage(src) // src: string url | FetchResponse | File | SvgSource | Canvas (se
1056
1062
  // .setSourceRect(x, y, w, h) — crop to a sub-rect of the source (texture px); frame fills the box, objectFit
1057
1063
  // ignored. Chainable, safe to call before mount. For atlases/spritesheets — swap the rect to change frame.
1058
1064
  // Note: tintColor applies only to SVG sources — it replaces ALL fill and stroke colors (monochrome icons only)
1059
- // Note: a bare relative path won't resolve — plain strings are https:// URLs; project files via import or asset('./photo.png')
1065
+ // Note: a project file by its path as a string — UIImage("images/photo.png") — is the bundled asset (the compile resolves it; a path that names no file fails the compile); plain https:// strings are remote URLs
1060
1066
 
1061
1067
  // UIVideo — the on-screen surface only; playback lives entirely on the VideoPlayer (see AUDIO / VIDEO above)
1062
1068
  UIVideo(player)
@@ -1086,6 +1092,7 @@ UIInput() / UITextArea()
1086
1092
  // type: "text"|"password"|"search"|"phone"|"email"|"number"|"decimal"|"url"|"date"|"time"
1087
1093
  // enterKey: "done"|"go"|"next"|"search"|"send" (UIInput only) — return-key label
1088
1094
  // maxLength: number; autocapitalize: "none"|"words"|"sentences"|"characters"; autocorrect: boolean
1095
+ // autofocus: boolean — takes the focus as its screen opens (a composer, a chat, a search; once, one per screen)
1089
1096
  // keyboardShrink: boolean (default true) — false: keyboard OVERLAYS the UI, no relayout (chat composers)
1090
1097
  // keyboardDismiss: boolean (default true) — false: taps never dismiss the keyboard (chat composers)
1091
1098
  // $focused: { borderColor, ..., duration? } — style while focused (reserved class)
@@ -1361,6 +1368,8 @@ UIImage(heart).style({ width: 24, height: 24, tintColor: "#666" }) // tintColor
1361
1368
  // ===== ICONS (assetIcon) =====
1362
1369
  // Use real icons instead of emoji. assetIcon("pack:name") resolves the icon at COMPILE time and
1363
1370
  // inlines it as an image source (same shape as SvgSource) — no imports, no project files, offline.
1371
+ // It returns an SvgSource — the same type an imported .svg and SvgSource(xml) give — so a component
1372
+ // that takes an icon is typed (icon: SvgSource), never (icon: string).
1364
1373
  UIImage(assetIcon("lucide:bell")).style({ width: 24, height: 24, tintColor: "#8a8f98" })
1365
1374
  // The id must be a string literal; an unknown id is a compile error (with name suggestions).
1366
1375
  // Recolor via the tintColor style or the { color } option (hex literal bakes in; expression tints).
@@ -1415,12 +1424,6 @@ UIRow(isGroup ? button : UIColumn()) // WRONG
1415
1424
  // ✅ null is skipped in children — no phantom element
1416
1425
  UIRow(isGroup ? button : null)
1417
1426
 
1418
- // ❌ pointing UIImage / bgImage at a project file by bare path — it won't resolve to the bundled asset
1419
- UIImage("./photo.png") // WRONG (a plain string works only for remote http(s) URLs)
1420
- // ✅ import the asset, or wrap its path in asset()
1421
- import photo from './photo.png'
1422
- UIImage(photo) // or: UIImage(asset('./photo.png'))
1423
-
1424
1427
  // ❌ callback-first edge callbacks on UIVirtualizedList
1425
1428
  list.onEndReached(loadNextPage) // WRONG — threshold is the first argument, and required
1426
1429
  // ✅ threshold (px from the edge) first
@@ -112,7 +112,7 @@ Everything in this prompt is a global — no imports needed. A project's only im
112
112
 
113
113
  Imports of the project's own files are also managed automatically: if your <edit> or <replace> makes code reference another file's export, the import is added for you — never fall back to a whole <file> rewrite just to change import lines. When writing a complete <file>, include imports normally.
114
114
 
115
- Reference a project asset (image, font, video, .svg, .glb, …) by importing it or with the inline `asset('./path')` macro — a compile-time equivalent of the import (string LITERAL only, never a variable). External `https://…` URLs are used directly as strings.
115
+ Reference a project asset (image, font, video, .svg, .glb, …) by its path as a string LITERAL — `UIImage("images/hero.png")`, `"./x.png"` from the file, `"/x.png"` from the root — or with the inline `asset('./path')` macro; both are a compile-time equivalent of the import (a literal, never a variable; a path that names no file fails the compile). A `.json` path stays a string. External `https://…` URLs are used directly as strings.
116
116
 
117
117
  ## Project context
118
118
 
@@ -229,6 +229,7 @@ new WebSocket(url, headers?: Record<string, string>)
229
229
  // ===== STORAGE / DEVICE / MISC =====
230
230
 
231
231
  localStorage.getItem(key): string | null / .setItem(key, value) / .removeItem(key) // nothing else
232
+ signal<Line[]>([]).stored("cart") // a signal kept on the device under the key (JSON values): back on the next launch
232
233
 
233
234
  device.platform: "ios" | "android" | "web" // getter, no ()
234
235
  device.language: string // "en", "ru", …
@@ -812,6 +813,8 @@ const tabs = UITabs({
812
813
  Router.init(tabs) // UITabs IS a UIScreen — present it directly
813
814
  // .select(id), .tab (getter), .onSelect(cb(id, i)) — fires on a bar tap, swipe, or select()
814
815
  // .badge(id, value) — true = dot, number/string = count pill, false/null/0 clears
816
+ // Ambient, from any screen, no reference threading: UITabs.current (the visible tabs, or null) and
817
+ // UITabs.select("profile") — the twin of UIPager.current / UIPager.push
815
818
  // .pager — the UIPager underneath; UIPager.push(detail) from any screen keeps the bar
816
819
  // The bar is colored by the theme's roles (accent, textMuted, surface, border) — no keys of its
817
820
  // own; one bar's own colors: tabs.bar.theme({ accent, surface }). Custom layout: UIPager below.
@@ -894,17 +897,17 @@ UIScrollable(...children)
894
897
  // visible rows (plus a buffer) are mounted. Use it instead of UIScrollable + map() whenever the item count
895
898
  // is large, grows over time, or is unknown.
896
899
  UIVirtualizedList<T>({
900
+ data?: () => T[], // the rows, read reactively: write the signal, the list follows (new array, keys matched)
901
+ // absent = fed by hand: .setData(items) / .append(...items) / .prepend(...items) / .update(...items) / .removeByKey(...keys)
902
+ // (one feed per list: a list with `data` has no append on its type)
897
903
  keyOf: (item: T) => string, // STABLE unique id per item (never the array index)
898
904
  render: (item: T) => UINodeChild, // builds one row; called lazily as rows enter the window
899
905
  estimatedHeight: number | ((item: T) => number), // px guess per row (real height measured after mount)
900
906
  overscan?: number, // extra px mounted above/below the viewport (default: one viewport)
901
907
  inverted?: boolean, // true = chat mode: starts scrolled to the end, append at bottom auto-scrolls
902
908
  })
903
- // Data is driven IMPERATIVELY (no diffing) — call these, never re-create the list:
904
- // .setData(items: T[]) // replace everything (diffed by key)
905
- // .append(...items) / .prepend(...items) // variadic; append = newest chat msg; prepend = history, no scroll jump
906
- // .update(...items) // re-render rows with the same keyOf(); unknown keys ignored
907
- // .removeByKey(...keys)
909
+ // Rows added at the end scroll a chat at its bottom; rows added before the first (history) move nothing; a row
910
+ // whose item is a new object re-renders. Never re-create the list.
908
911
  // .itemCount (getter) / .getItem(key)
909
912
  // .scrollTo(offset, animated?) / .scrollToKey(key, animated?) / .scrollToEnd(animated?) / .onScroll(cb(pos))
910
913
  // .onEndReached(thresholdPx: number, cb) // threshold FIRST and required; fires near the BOTTOM — load next page
@@ -932,7 +935,7 @@ UIImage(src) // src: string url | FetchResponse | File | SvgSource | Canvas (se
932
935
  // .setSourceRect(x, y, w, h) — crop to a sub-rect of the source (texture px); frame fills the box, objectFit
933
936
  // ignored. Chainable, safe to call before mount. For atlases/spritesheets — swap the rect to change frame.
934
937
  // Note: tintColor applies only to SVG sources — it replaces ALL fill and stroke colors (monochrome icons only)
935
- // Note: a bare relative path won't resolve — plain strings are https:// URLs; project files via import or asset('./photo.png')
938
+ // Note: a project file by its path as a string — UIImage("images/photo.png") — is the bundled asset (the compile resolves it; a path that names no file fails the compile); plain https:// strings are remote URLs
936
939
 
937
940
  // UIVideo — the on-screen surface only; playback lives entirely on the VideoPlayer (see AUDIO / VIDEO above)
938
941
  UIVideo(player)
@@ -962,6 +965,7 @@ UIInput() / UITextArea()
962
965
  // type: "text"|"password"|"search"|"phone"|"email"|"number"|"decimal"|"url"|"date"|"time"
963
966
  // enterKey: "done"|"go"|"next"|"search"|"send" (UIInput only) — return-key label
964
967
  // maxLength: number; autocapitalize: "none"|"words"|"sentences"|"characters"; autocorrect: boolean
968
+ // autofocus: boolean — takes the focus as its screen opens (a composer, a chat, a search; once, one per screen)
965
969
  // keyboardShrink: boolean (default true) — false: keyboard OVERLAYS the UI, no relayout (chat composers)
966
970
  // keyboardDismiss: boolean (default true) — false: taps never dismiss the keyboard (chat composers)
967
971
  // $focused: { borderColor, ..., duration? } — style while focused (reserved class)
@@ -1237,6 +1241,8 @@ UIImage(heart).style({ width: 24, height: 24, tintColor: "#666" }) // tintColor
1237
1241
  // ===== ICONS (assetIcon) =====
1238
1242
  // Use real icons instead of emoji. assetIcon("pack:name") resolves the icon at COMPILE time and
1239
1243
  // inlines it as an image source (same shape as SvgSource) — no imports, no project files, offline.
1244
+ // It returns an SvgSource — the same type an imported .svg and SvgSource(xml) give — so a component
1245
+ // that takes an icon is typed (icon: SvgSource), never (icon: string).
1240
1246
  UIImage(assetIcon("lucide:bell")).style({ width: 24, height: 24, tintColor: "#8a8f98" })
1241
1247
  // The id must be a string literal; an unknown id is a compile error (with name suggestions).
1242
1248
  // Recolor via the tintColor style or the { color } option (hex literal bakes in; expression tints).
@@ -1291,12 +1297,6 @@ UIRow(isGroup ? button : UIColumn()) // WRONG
1291
1297
  // ✅ null is skipped in children — no phantom element
1292
1298
  UIRow(isGroup ? button : null)
1293
1299
 
1294
- // ❌ pointing UIImage / bgImage at a project file by bare path — it won't resolve to the bundled asset
1295
- UIImage("./photo.png") // WRONG (a plain string works only for remote http(s) URLs)
1296
- // ✅ import the asset, or wrap its path in asset()
1297
- import photo from './photo.png'
1298
- UIImage(photo) // or: UIImage(asset('./photo.png'))
1299
-
1300
1300
  // ❌ callback-first edge callbacks on UIVirtualizedList
1301
1301
  list.onEndReached(loadNextPage) // WRONG — threshold is the first argument, and required
1302
1302
  // ✅ threshold (px from the edge) first
@@ -112,7 +112,7 @@ Everything in this prompt is a global — no imports needed. A project's only im
112
112
 
113
113
  Imports of the project's own files are also managed automatically: if your <edit> or <replace> makes code reference another file's export, the import is added for you — never fall back to a whole <file> rewrite just to change import lines. When writing a complete <file>, include imports normally.
114
114
 
115
- Reference a project asset (image, font, video, .svg, .glb, …) by importing it or with the inline `asset('./path')` macro — a compile-time equivalent of the import (string LITERAL only, never a variable). External `https://…` URLs are used directly as strings.
115
+ Reference a project asset (image, font, video, .svg, .glb, …) by its path as a string LITERAL — `UIImage("images/hero.png")`, `"./x.png"` from the file, `"/x.png"` from the root — or with the inline `asset('./path')` macro; both are a compile-time equivalent of the import (a literal, never a variable; a path that names no file fails the compile). A `.json` path stays a string. External `https://…` URLs are used directly as strings.
116
116
 
117
117
  ## Project context
118
118
 
@@ -229,6 +229,7 @@ new WebSocket(url, headers?: Record<string, string>)
229
229
  // ===== STORAGE / DEVICE / MISC =====
230
230
 
231
231
  localStorage.getItem(key): string | null / .setItem(key, value) / .removeItem(key) // nothing else
232
+ signal<Line[]>([]).stored("cart") // a signal kept on the device under the key (JSON values): back on the next launch
232
233
 
233
234
  device.platform: "ios" | "android" | "web" // getter, no ()
234
235
  device.language: string // "en", "ru", …
@@ -402,6 +403,8 @@ const tabs = UITabs({
402
403
  Router.init(tabs) // UITabs IS a UIScreen — present it directly
403
404
  // .select(id), .tab (getter), .onSelect(cb(id, i)) — fires on a bar tap, swipe, or select()
404
405
  // .badge(id, value) — true = dot, number/string = count pill, false/null/0 clears
406
+ // Ambient, from any screen, no reference threading: UITabs.current (the visible tabs, or null) and
407
+ // UITabs.select("profile") — the twin of UIPager.current / UIPager.push
405
408
  // .pager — the UIPager underneath; UIPager.push(detail) from any screen keeps the bar
406
409
  // The bar is colored by the theme's roles (accent, textMuted, surface, border) — no keys of its
407
410
  // own; one bar's own colors: tabs.bar.theme({ accent, surface }). Custom layout: UIPager below.
@@ -484,17 +487,17 @@ UIScrollable(...children)
484
487
  // visible rows (plus a buffer) are mounted. Use it instead of UIScrollable + map() whenever the item count
485
488
  // is large, grows over time, or is unknown.
486
489
  UIVirtualizedList<T>({
490
+ data?: () => T[], // the rows, read reactively: write the signal, the list follows (new array, keys matched)
491
+ // absent = fed by hand: .setData(items) / .append(...items) / .prepend(...items) / .update(...items) / .removeByKey(...keys)
492
+ // (one feed per list: a list with `data` has no append on its type)
487
493
  keyOf: (item: T) => string, // STABLE unique id per item (never the array index)
488
494
  render: (item: T) => UINodeChild, // builds one row; called lazily as rows enter the window
489
495
  estimatedHeight: number | ((item: T) => number), // px guess per row (real height measured after mount)
490
496
  overscan?: number, // extra px mounted above/below the viewport (default: one viewport)
491
497
  inverted?: boolean, // true = chat mode: starts scrolled to the end, append at bottom auto-scrolls
492
498
  })
493
- // Data is driven IMPERATIVELY (no diffing) — call these, never re-create the list:
494
- // .setData(items: T[]) // replace everything (diffed by key)
495
- // .append(...items) / .prepend(...items) // variadic; append = newest chat msg; prepend = history, no scroll jump
496
- // .update(...items) // re-render rows with the same keyOf(); unknown keys ignored
497
- // .removeByKey(...keys)
499
+ // Rows added at the end scroll a chat at its bottom; rows added before the first (history) move nothing; a row
500
+ // whose item is a new object re-renders. Never re-create the list.
498
501
  // .itemCount (getter) / .getItem(key)
499
502
  // .scrollTo(offset, animated?) / .scrollToKey(key, animated?) / .scrollToEnd(animated?) / .onScroll(cb(pos))
500
503
  // .onEndReached(thresholdPx: number, cb) // threshold FIRST and required; fires near the BOTTOM — load next page
@@ -522,7 +525,7 @@ UIImage(src) // src: string url | FetchResponse | File | SvgSource | Canvas (se
522
525
  // .setSourceRect(x, y, w, h) — crop to a sub-rect of the source (texture px); frame fills the box, objectFit
523
526
  // ignored. Chainable, safe to call before mount. For atlases/spritesheets — swap the rect to change frame.
524
527
  // Note: tintColor applies only to SVG sources — it replaces ALL fill and stroke colors (monochrome icons only)
525
- // Note: a bare relative path won't resolve — plain strings are https:// URLs; project files via import or asset('./photo.png')
528
+ // Note: a project file by its path as a string — UIImage("images/photo.png") — is the bundled asset (the compile resolves it; a path that names no file fails the compile); plain https:// strings are remote URLs
526
529
 
527
530
  // UIVideo — the on-screen surface only; playback lives entirely on the VideoPlayer (see AUDIO / VIDEO above)
528
531
  UIVideo(player)
@@ -552,6 +555,7 @@ UIInput() / UITextArea()
552
555
  // type: "text"|"password"|"search"|"phone"|"email"|"number"|"decimal"|"url"|"date"|"time"
553
556
  // enterKey: "done"|"go"|"next"|"search"|"send" (UIInput only) — return-key label
554
557
  // maxLength: number; autocapitalize: "none"|"words"|"sentences"|"characters"; autocorrect: boolean
558
+ // autofocus: boolean — takes the focus as its screen opens (a composer, a chat, a search; once, one per screen)
555
559
  // keyboardShrink: boolean (default true) — false: keyboard OVERLAYS the UI, no relayout (chat composers)
556
560
  // keyboardDismiss: boolean (default true) — false: taps never dismiss the keyboard (chat composers)
557
561
  // $focused: { borderColor, ..., duration? } — style while focused (reserved class)
@@ -827,6 +831,8 @@ UIImage(heart).style({ width: 24, height: 24, tintColor: "#666" }) // tintColor
827
831
  // ===== ICONS (assetIcon) =====
828
832
  // Use real icons instead of emoji. assetIcon("pack:name") resolves the icon at COMPILE time and
829
833
  // inlines it as an image source (same shape as SvgSource) — no imports, no project files, offline.
834
+ // It returns an SvgSource — the same type an imported .svg and SvgSource(xml) give — so a component
835
+ // that takes an icon is typed (icon: SvgSource), never (icon: string).
830
836
  UIImage(assetIcon("lucide:bell")).style({ width: 24, height: 24, tintColor: "#8a8f98" })
831
837
  // The id must be a string literal; an unknown id is a compile error (with name suggestions).
832
838
  // Recolor via the tintColor style or the { color } option (hex literal bakes in; expression tints).
@@ -881,12 +887,6 @@ UIRow(isGroup ? button : UIColumn()) // WRONG
881
887
  // ✅ null is skipped in children — no phantom element
882
888
  UIRow(isGroup ? button : null)
883
889
 
884
- // ❌ pointing UIImage / bgImage at a project file by bare path — it won't resolve to the bundled asset
885
- UIImage("./photo.png") // WRONG (a plain string works only for remote http(s) URLs)
886
- // ✅ import the asset, or wrap its path in asset()
887
- import photo from './photo.png'
888
- UIImage(photo) // or: UIImage(asset('./photo.png'))
889
-
890
890
  // ❌ callback-first edge callbacks on UIVirtualizedList
891
891
  list.onEndReached(loadNextPage) // WRONG — threshold is the first argument, and required
892
892
  // ✅ threshold (px from the edge) first
package/src/chisel.ts CHANGED
@@ -130,7 +130,7 @@ export const chiselVocab: ChiselVocab = {
130
130
  "onScroll", "onScrollRelease", "onOverscroll", "onRefresh", "onSubmit",
131
131
  "onSelect", "onChange", "onFocus", "onBlur", "onOverlayTap", "onDetent",
132
132
  "onEndReached", "onStartReached",
133
- "append", "insert", "remove", "setContent", "keepAlive", "url",
133
+ "append", "insert", "remove", "setContent", "widgets", "keepAlive", "url",
134
134
  "detents", "dismissible", "transition", "setSourceRect",
135
135
  "push", "pop", "popToRoot", "replace", "select",
136
136
  // UITabs / UIVirtualizedList's hand feed / the native views (NativeView, CameraView, MapView, QRScanner)
package/src/g2/Scene2D.ts CHANGED
@@ -6,7 +6,7 @@ import { _installAspectFrames, _installTimeScale, _attachSystem, _detachSystem,
6
6
  import { _bumpNavEpoch, _setCurrent, Presentable, type DestTuple, type DismissOptions, type PresentOptions } from "../ui/presentable"
7
7
  import { _transitionId, TRANSITION_NONE } from "../ui/transitions"
8
8
  import { TREE_DEST_SCENE2D, ensureEmitter, registerDest } from "../ui/tree"
9
- import { _setSurfaceContent, type LayerElement, type UIWidgetContent } from "../ui/UILayer"
9
+ import { _surfaceWidgets, type LayerElement, type WidgetSet } from "../ui/UILayer"
10
10
  import { cx, cy, type Vec2Like } from "../math/vec"
11
11
  import type { ClickEvent, TouchStartEvent } from "../runtime/touch"
12
12
  import { Camera2D } from "./Camera2D"
@@ -189,14 +189,16 @@ export class Scene2D implements Presentable {
189
189
  registerDest(TREE_DEST_SCENE2D, this.id, this)
190
190
  return [TREE_DEST_SCENE2D, this.id, "", ""]
191
191
  }
192
- /** @internal The scene's content layer (ui/UILayer.ts), made by the first setContent. */
192
+ /** @internal The scene's widget layer (ui/UILayer.ts), made on the first use of `widgets`. */
193
193
  _contentLayer?: LayerElement
194
- /** The UI over the scene — widgets (a HUD, a dialog), laid out over the scene while it is
195
- * presented; touches outside a widget's box reach the scene. */
196
- setContent(content: UIWidgetContent): this {
194
+ /** @internal */
195
+ _widgetSet?: WidgetSet<any>
196
+ /** The widgets over the scene while it is presented — a HUD, a dialog, a sheet: `scene.widgets(hud)`
197
+ * sets them, `widgets.append` / `.remove` manage them, `widgets.list` reads them. Laid out
198
+ * over it; touches outside a widget's box reach the scene. */
199
+ get widgets(): WidgetSet<this> {
197
200
  ensureEmitter()
198
- _setSurfaceContent(this, content)
199
- return this
201
+ return _surfaceWidgets(this)
200
202
  }
201
203
  /** @internal presentation lifecycle from the runtime */
202
204
  _emitOpen(): void { for (const cb of this.ol.slice()) cb() }
package/src/gl/Scene.ts CHANGED
@@ -10,7 +10,7 @@ import { _installAspectFrames, _installTimeScale, _attachSystem, _detachSystem,
10
10
  import { _bumpNavEpoch, _navEpoch, _setCurrent, Presentable, type DestTuple, type DismissOptions, type PresentOptions } from "../ui/presentable"
11
11
  import { _transitionId, TRANSITION_NONE } from "../ui/transitions"
12
12
  import { TREE_DEST_SCENE3D, TREE_FLAG_BACK, ensureEmitter, registerDest, unregisterDest } from "../ui/tree"
13
- import { _setSurfaceContent, type LayerElement, type UIWidgetContent } from "../ui/UILayer"
13
+ import { _surfaceWidgets, type LayerElement, type WidgetSet } from "../ui/UILayer"
14
14
  import { device } from "../runtime/device"
15
15
  import type { ClickEvent, TouchStartEvent } from "../runtime/touch"
16
16
  import type { FetchResponse } from "../runtime/fetch"
@@ -458,14 +458,16 @@ export class Scene implements Presentable {
458
458
  registerDest(TREE_DEST_SCENE3D, this._id, this)
459
459
  return [TREE_DEST_SCENE3D, this._id, "", ""]
460
460
  }
461
- /** @internal The scene's content layer (ui/UILayer.ts), made by the first setContent. */
461
+ /** @internal The scene's widget layer (ui/UILayer.ts), made on the first use of `widgets`. */
462
462
  _contentLayer?: LayerElement
463
- /** The UI over the scene — widgets (a HUD, a dialog), laid out over the scene while it is
464
- * presented; touches outside a widget's box reach the scene. `scene.setContent([hud])`. */
465
- setContent(content: UIWidgetContent): this {
463
+ /** @internal */
464
+ _widgetSet?: WidgetSet<any>
465
+ /** The widgets over the scene while it is presented — a HUD, a dialog, a sheet: `scene.widgets(hud)`
466
+ * sets them, `widgets.append` / `.remove` manage them, `widgets.list` reads them. Laid out
467
+ * over it; touches outside a widget's box reach the scene. */
468
+ get widgets(): WidgetSet<this> {
466
469
  ensureEmitter()
467
- _setSurfaceContent(this, content)
468
- return this
470
+ return _surfaceWidgets(this)
469
471
  }
470
472
  /** @internal presentation lifecycle from the runtime */
471
473
  _emitOpen(): void { for (const cb of this.ol.slice()) cb() }
package/src/inject.ts CHANGED
@@ -104,7 +104,7 @@ export {
104
104
  UITabs, type UITabDef,
105
105
  defineTabs, type TabDef, type TabsHandle,
106
106
  UIWidget, type UIWidgetStyle,
107
- UIOverlay, type UIWidgetContent,
107
+ type WidgetSet, type UIWidgetContent, type UIWidgetChild,
108
108
  UIModal, type UIModalStyle,
109
109
  UIBottomSheet, type UIBottomSheetStyle,
110
110
  UIPopover, type UIPopoverStyle,
@@ -10,7 +10,7 @@
10
10
  // wrapper convention.
11
11
 
12
12
  import { _channelCall, _channelEmit, _channelOff, _channelOn, type ChannelListeners } from "../runtime/channel"
13
- import { _setSurfaceContent, type LayerElement, type UIWidgetContent } from "./UILayer"
13
+ import { _surfaceWidgets, type LayerElement, type WidgetSet } from "./UILayer"
14
14
  import { _bumpNavEpoch, _setCurrent, Presentable, type DestTuple, type DismissOptions, type PresentOptions } from "./presentable"
15
15
  import { _transitionId, TRANSITION_NONE } from "./transitions"
16
16
  import { Element, type BaseStyle, type DrawableStyle, type ElementStyle, type UIElementBase } from "./UINode"
@@ -46,8 +46,8 @@ export interface NativeView extends UIElementBase<UINativeViewStyle, DrawableSty
46
46
  /** Subscribe to an event the native view emits (`map.on("markerTap", cb)`). */
47
47
  on(event: string, callback: (data?: any) => void): this
48
48
  off(event: string, callback: (data?: any) => void): this
49
- /** The UI over the view when presented: widgets over it (`Presentable.setContent`). */
50
- setContent(content: UIWidgetContent): this
49
+ /** The widgets over the view when it is presented (`Presentable.widgets`): `map.widgets(header, card)`. */
50
+ readonly widgets: WidgetSet<this>
51
51
  /** @internal The destination tuple (Presentable). */
52
52
  _dest(): DestTuple
53
53
  }
@@ -88,13 +88,15 @@ export class NativeViewElement extends Element<"native"> implements Presentable
88
88
 
89
89
  /** @internal */
90
90
  _dest(): DestTuple { return [TREE_DEST_NATIVE, this._viewId, this.viewName, this._paramsJson] }
91
- /** @internal The view's content layer (ui/UILayer.ts), made by the first setContent. */
91
+ /** @internal The view's widget layer (ui/UILayer.ts), made on the first use of `widgets`. */
92
92
  _contentLayer?: LayerElement
93
- /** The UI over the view when it is presented full screen — widgets (a header, a card, a sheet
94
- * over a map), laid out over it; touches outside a widget's box reach the view. */
95
- setContent(content: UIWidgetContent): this {
96
- _setSurfaceContent(this, content)
97
- return this
93
+ /** @internal */
94
+ _widgetSet?: WidgetSet<any>
95
+ /** The widgets over the view while it is presented — a HUD, a dialog, a sheet: `map.widgets(hud)`
96
+ * sets them, `widgets.append` / `.remove` manage them, `widgets.list` reads them. Laid out
97
+ * over it; touches outside a widget's box reach the view. */
98
+ get widgets(): WidgetSet<this> {
99
+ return _surfaceWidgets(this)
98
100
  }
99
101
 
100
102
  open(options?: PresentOptions): void {
package/src/ui/UI.ts CHANGED
@@ -10,7 +10,7 @@ export { UIVideo, type UIVideoStyle } from './UIVideo'
10
10
  export { UIInput, UITextArea, type UIInputStyle } from './UIInput'
11
11
 
12
12
  export { UIWidget, type UIWidgetStyle } from './UIWidget'
13
- export { UIOverlay, type UIWidgetContent } from './UILayer'
13
+ export { type WidgetSet, type UIWidgetContent, type UIWidgetChild } from './UILayer'
14
14
  export { UIModal, type UIModalStyle } from './UIModal'
15
15
  export { UIBottomSheet, type UIBottomSheetStyle } from './UIBottomSheet'
16
16
  export { UIPopover, type UIPopoverStyle, type UIPopoverAnchor } from './UIPopover'