lecodes-sdk 1.1.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -2
- package/dist/editor.d.ts +12 -0
- package/dist/global.d.ts +4 -8
- package/dist/host.d.ts +2 -3
- package/dist/types/animate/tween/Animation.d.ts +0 -3
- package/dist/types/animate/tween/animateValue.d.ts +4 -2
- package/dist/types/animate/tween/easing.d.ts +8 -0
- package/dist/types/animate/tween/spec.d.ts +15 -7
- package/dist/types/audio/audio.d.ts +2 -1
- package/dist/types/canvas/Canvas.d.ts +40 -108
- package/dist/types/canvas/gen/cssColor.d.ts +17 -0
- package/dist/types/canvas/gen/recorder.d.ts +118 -0
- package/dist/types/canvas/gen/spec.d.ts +144 -0
- package/dist/types/core/color.d.ts +3 -1
- package/dist/types/core/pins.d.ts +18 -0
- package/dist/types/g2/Node2D.d.ts +5 -8
- package/dist/types/g2/Scene2D.d.ts +6 -2
- package/dist/types/gl/Foliage.d.ts +30 -7
- package/dist/types/gl/Light.d.ts +11 -0
- package/dist/types/gl/Lightmap.d.ts +13 -2
- package/dist/types/gl/Material.d.ts +17 -3
- package/dist/types/gl/Model.d.ts +6 -2
- package/dist/types/gl/Node.d.ts +3 -6
- package/dist/types/gl/Particles.d.ts +15 -0
- package/dist/types/gl/Scene.d.ts +41 -13
- package/dist/types/gl/Texture.d.ts +1 -1
- package/dist/types/gl/animation/Locomotion.d.ts +59 -3
- package/dist/types/gl/terrain/Terrain.d.ts +8 -0
- package/dist/types/inject.d.ts +11 -11
- package/dist/types/inject.editor.d.ts +1 -0
- package/dist/types/net/core.d.ts +7 -0
- package/dist/types/plugin.d.ts +76 -0
- package/dist/types/plugins/gen/camera/sdk/camera.d.ts +24 -0
- package/dist/types/plugins/gen/camera/sdk/camera.gen.d.ts +25 -0
- package/dist/types/plugins/{geolocation.d.ts → gen/geolocation/sdk/geolocation.d.ts} +2 -20
- package/dist/types/plugins/gen/geolocation/sdk/geolocation.gen.d.ts +31 -0
- package/dist/types/plugins/{map.d.ts → gen/map/sdk/map.d.ts} +9 -50
- package/dist/types/plugins/gen/map/sdk/map.gen.d.ts +53 -0
- package/dist/types/plugins/gen/push/sdk/push.d.ts +23 -0
- package/dist/types/plugins/gen/push/sdk/push.gen.d.ts +35 -0
- package/dist/types/plugins/{qr.d.ts → gen/qr-scanner/sdk/qr-scanner.d.ts} +2 -3
- package/dist/types/plugins/gen/qr-scanner/sdk/qr-scanner.gen.d.ts +15 -0
- package/dist/types/runtime/app.d.ts +9 -2
- package/dist/types/runtime/fetch.d.ts +2 -0
- package/dist/types/runtime/input.d.ts +8 -1
- package/dist/types/runtime/media.d.ts +6 -10
- package/dist/types/runtime/misc.d.ts +4 -1
- package/dist/types/runtime/net.d.ts +3 -2
- package/dist/types/runtime/touch.d.ts +32 -0
- package/dist/types/scene/defineScene.d.ts +43 -2
- package/dist/types/scene/editor.d.ts +52 -0
- package/dist/types/scene/gizmos.d.ts +7 -4
- package/dist/types/ui/NativeView.d.ts +6 -4
- package/dist/types/ui/UI.d.ts +1 -1
- package/dist/types/ui/UIBottomSheet.d.ts +6 -12
- package/dist/types/ui/UIButton.d.ts +12 -14
- package/dist/types/ui/UIContainer.d.ts +0 -6
- package/dist/types/ui/UIImage.d.ts +1 -4
- package/dist/types/ui/UIInput.d.ts +8 -24
- package/dist/types/ui/UIModal.d.ts +0 -2
- package/dist/types/ui/UINode.d.ts +70 -56
- package/dist/types/ui/UIPager.d.ts +28 -27
- package/dist/types/ui/UIPopover.d.ts +0 -2
- package/dist/types/ui/UIScreen.d.ts +12 -17
- package/dist/types/ui/UIScrollable.d.ts +1 -4
- package/dist/types/ui/UIText.d.ts +0 -2
- package/dist/types/ui/UIVideo.d.ts +3 -5
- package/dist/types/ui/UIVirtualizedList.d.ts +14 -16
- package/dist/types/ui/UIWidget.d.ts +10 -9
- package/dist/types/ui/colorKeys.gen.d.ts +9 -0
- package/dist/types/ui/presentable.d.ts +46 -32
- package/dist/types/ui/router.d.ts +18 -7
- package/dist/types/ui/styleColor.d.ts +1 -0
- package/dist/types/ui/transitions.d.ts +18 -0
- package/dist/types/ui/tree.d.ts +75 -0
- package/dist/types/version.d.ts +10 -0
- package/dist/types.json +1 -1
- package/package.json +12 -3
- package/prompts/2d.md +2 -6
- package/prompts/3d.md +1 -5
- package/prompts/README.md +142 -142
- package/prompts/canvas.md +9 -8
- package/prompts/compose.ts +1 -1
- package/prompts/core.md +3 -3
- package/prompts/design.md +1 -1
- package/prompts/dist/2d-game.md +77 -54
- package/prompts/dist/3d-app.md +75 -52
- package/prompts/dist/ar-app.md +74 -51
- package/prompts/dist/design.md +30 -8
- package/prompts/dist/ui-app.md +73 -46
- package/prompts/ui-design.md +2 -3
- package/prompts/ui.md +45 -37
- package/src/animate/tween/Animation.ts +262 -378
- package/src/animate/tween/animateValue.ts +103 -100
- package/src/animate/tween/easing.ts +10 -3
- package/src/animate/tween/spec.ts +505 -479
- package/src/audio/Sound.ts +3 -3
- package/src/audio/audio.ts +162 -161
- package/src/bridges/2d.d.ts +317 -0
- package/src/bridges/app.d.ts +91 -0
- package/src/bridges/audio.d.ts +97 -0
- package/src/bridges/canvas.d.ts +79 -0
- package/src/bridges/device.d.ts +72 -0
- package/src/bridges/fetch.d.ts +80 -0
- package/src/bridges/files.d.ts +70 -0
- package/src/bridges/gl.d.ts +1133 -0
- package/src/bridges/input.d.ts +72 -0
- package/src/bridges/media.d.ts +55 -0
- package/src/bridges/nav.d.ts +71 -0
- package/src/bridges/net.d.ts +52 -0
- package/src/bridges/service.d.ts +52 -0
- package/src/bridges/socket.d.ts +31 -0
- package/src/bridges/storage.d.ts +33 -0
- package/src/bridges/tree.d.ts +301 -0
- package/src/bridges/types.d.ts +49 -0
- package/src/canvas/Canvas.ts +114 -159
- package/src/canvas/gen/cssColor.ts +224 -0
- package/src/canvas/gen/recorder.ts +212 -0
- package/src/canvas/gen/spec.ts +201 -0
- package/src/chisel.ts +193 -0
- package/src/compile/assetMacro.ts +1 -1
- package/src/compile/bundler.ts +11 -2
- package/src/compile/compileProject.ts +43 -4
- package/src/compile/fontMacro.ts +3 -4
- package/src/compile/header.ts +26 -5
- package/src/compile/index.ts +3 -1
- package/src/compile/liteMaterial.ts +1 -1
- package/src/compile/sceneEditor.ts +11 -26
- package/src/core/color.ts +73 -30
- package/src/core/pins.ts +51 -0
- package/src/core/signals.ts +8 -1
- package/src/g2/CharacterController2D.ts +3 -3
- package/src/g2/Node2D.ts +57 -39
- package/src/g2/Physics2D.ts +2 -2
- package/src/g2/Scene2D.ts +35 -23
- package/src/g2/Texture2D.ts +1 -1
- package/src/g2/loop.ts +4 -4
- package/src/gl/Foliage.ts +72 -17
- package/src/gl/Geometry.ts +1 -2
- package/src/gl/Light.ts +13 -0
- package/src/gl/Lightmap.ts +45 -29
- package/src/gl/Material.ts +95 -51
- package/src/gl/Model.ts +172 -167
- package/src/gl/Node.ts +91 -24
- package/src/gl/Particles.ts +20 -1
- package/src/gl/Scene.ts +100 -44
- package/src/gl/Texture.ts +8 -7
- package/src/gl/animation/AnimationClip.ts +1 -1
- package/src/gl/animation/Locomotion.ts +77 -8
- package/src/gl/nav/NavMesh.ts +3 -4
- package/src/gl/physics/Physics.ts +2 -2
- package/src/gl/physics/physicsEvents.ts +3 -3
- package/src/gl/terrain/Terrain.ts +33 -5
- package/src/gl/touch.ts +14 -15
- package/src/host.d.ts +2 -3
- package/src/inject.editor.ts +7 -0
- package/src/inject.ts +233 -236
- package/src/net/core.ts +6 -5
- package/src/net/index.ts +1 -1
- package/src/net/replication.ts +1 -1
- package/src/plugin.ts +191 -0
- package/src/plugins/gen/camera/contract.d.ts +27 -0
- package/src/plugins/gen/camera/sdk/camera.gen.ts +46 -0
- package/src/plugins/gen/camera/sdk/camera.ts +57 -0
- package/src/plugins/gen/geolocation/contract.d.ts +50 -0
- package/src/plugins/gen/geolocation/sdk/geolocation.gen.ts +54 -0
- package/src/plugins/{geolocation.ts → gen/geolocation/sdk/geolocation.ts} +22 -43
- package/src/plugins/gen/map/contract.d.ts +144 -0
- package/src/plugins/gen/map/sdk/map.gen.ts +88 -0
- package/src/plugins/{map.ts → gen/map/sdk/map.ts} +68 -102
- package/src/plugins/gen/push/contract.d.ts +61 -0
- package/src/plugins/gen/push/sdk/push.gen.ts +60 -0
- package/src/plugins/gen/push/sdk/push.ts +105 -0
- package/src/plugins/gen/qr-scanner/contract.d.ts +16 -0
- package/src/plugins/gen/qr-scanner/sdk/qr-scanner.gen.ts +29 -0
- package/src/plugins/gen/qr-scanner/sdk/qr-scanner.ts +52 -0
- package/src/plugins/permission.ts +5 -4
- package/src/runtime/app.ts +20 -9
- package/src/runtime/appEvents.ts +5 -4
- package/src/runtime/channel.ts +18 -15
- package/src/runtime/clipboard.ts +4 -3
- package/src/runtime/datetime.ts +2 -1
- package/src/runtime/device.ts +17 -15
- package/src/runtime/fetch.ts +30 -20
- package/src/runtime/files.ts +16 -15
- package/src/runtime/input.ts +21 -8
- package/src/runtime/media.ts +50 -46
- package/src/runtime/misc.ts +7 -3
- package/src/runtime/net.ts +8 -7
- package/src/runtime/rpc.ts +1 -3
- package/src/runtime/service.ts +19 -14
- package/src/runtime/share.ts +4 -3
- package/src/runtime/storage.ts +6 -4
- package/src/runtime/touch.ts +32 -0
- package/src/scene/defineScene.ts +61 -365
- package/src/scene/editor.ts +408 -0
- package/src/scene/editorPlugins.ts +3 -3
- package/src/scene/gizmos.ts +154 -148
- package/src/server/db/marci/query.ts +1 -1
- package/src/server/host.ts +1 -1
- package/src/server/runtime.ts +1 -1
- package/src/ui/NativeView.ts +72 -25
- package/src/ui/UI.ts +3 -3
- package/src/ui/UIBottomSheet.ts +16 -17
- package/src/ui/UIButton.ts +54 -16
- package/src/ui/UIContainer.ts +0 -6
- package/src/ui/UIImage.ts +34 -30
- package/src/ui/UIInput.ts +29 -37
- package/src/ui/UIModal.ts +1 -3
- package/src/ui/UINode.ts +347 -297
- package/src/ui/UIPager.ts +93 -78
- package/src/ui/UIPopover.ts +0 -2
- package/src/ui/UIScreen.ts +41 -36
- package/src/ui/UIScrollable.ts +19 -13
- package/src/ui/UISpacer.ts +1 -1
- package/src/ui/UITabs.ts +8 -6
- package/src/ui/UIText.ts +7 -17
- package/src/ui/UIVideo.ts +30 -27
- package/src/ui/UIVirtualizedList.ts +58 -59
- package/src/ui/UIWidget.ts +38 -18
- package/src/ui/colorKeys.gen.ts +37 -0
- package/src/ui/fonts.ts +2 -2
- package/src/ui/presentable.ts +59 -42
- package/src/ui/router.ts +52 -35
- package/src/ui/styleColor.ts +56 -0
- package/src/ui/theme.ts +7 -6
- package/src/ui/transitions.ts +249 -0
- package/src/ui/tree.ts +346 -0
- package/src/version.ts +24 -0
- package/tests/helpers/engineWorld.ts +11 -0
- package/tests/helpers/fakeTree.ts +353 -0
- package/tests/helpers/hostStubs.ts +31 -0
- package/tests/helpers/index.ts +14 -0
- package/tests/helpers/memoryMarci.ts +124 -0
- package/tests/helpers/phases.ts +23 -0
- package/tests/helpers/preload.ts +18 -0
- package/tests/helpers/stubApp.ts +2 -0
- package/tests/helpers/stubDevice.ts +2 -0
- package/tests/helpers/stubFetch.ts +2 -0
- package/tests/helpers/stubInput.ts +2 -0
- package/dist/inject.js +0 -4730
- package/dist/types/core/registry.d.ts +0 -7
- package/dist/types/plugins/camera.d.ts +0 -25
- package/dist/types/plugins/push.d.ts +0 -46
- package/src/bridges.d.ts +0 -1760
- package/src/compile/__tests__/assetIconMacro.test.ts +0 -219
- package/src/compile/__tests__/assetMacro.test.ts +0 -100
- package/src/compile/__tests__/assetName.test.ts +0 -55
- package/src/compile/__tests__/compile.test.ts +0 -310
- package/src/compile/__tests__/detectEntry.test.ts +0 -151
- package/src/compile/__tests__/fontMacro.test.ts +0 -199
- package/src/compile/__tests__/serverSplit.test.ts +0 -27
- package/src/core/__tests__/stateMachine.test.ts +0 -132
- package/src/core/registry.ts +0 -23
- package/src/plugins/camera.ts +0 -81
- package/src/plugins/push.ts +0 -132
- package/src/plugins/qr.ts +0 -73
package/prompts/dist/ui-app.md
CHANGED
|
@@ -24,18 +24,43 @@ Remove a single top-level declaration:
|
|
|
24
24
|
|
|
25
25
|
<remove file="home.ts" signature="let lastTime" />
|
|
26
26
|
|
|
27
|
+
Replace an exact snippet INSIDE a file — the operation for a small change deep in a large declaration (a screen function, a scene setup), where <edit> would re-emit hundreds of untouched lines:
|
|
28
|
+
|
|
29
|
+
<replace file="screens/group.ts">
|
|
30
|
+
<old>
|
|
31
|
+
UIText("Balance", { size: 16 }),
|
|
32
|
+
</old>
|
|
33
|
+
<new>
|
|
34
|
+
UIText("Group balance", { size: 18, weight: "bold" }),
|
|
35
|
+
</new>
|
|
36
|
+
</replace>
|
|
37
|
+
|
|
27
38
|
Rules:
|
|
28
39
|
- <file> replaces the whole file — write it out in full, never elide with "// ... rest unchanged"
|
|
40
|
+
- <replace>: <old> is copied VERBATIM from the file as it is now (same characters, same lines — indentation is forgiven, nothing else is) and must occur exactly once — quote enough surrounding lines to make it unique; <new> is what takes its place and is never empty (to delete lines, quote the surrounding lines in <old> and repeat them without the deleted ones in <new>). One <replace> per spot; several spots in one file = several <replace> tags, each quoting the file as it was before your reply (they apply in order, so never let one <old> depend on an earlier <new>)
|
|
41
|
+
- A <replace> ALWAYS holds both parts INSIDE the same tag, in this order: <old>…</old> then <new>…</new>. A <replace> without those inner tags is invalid and applies nothing — never emit the old text in one <replace> and the new text in a second one
|
|
29
42
|
- <edit> and <remove> target exactly ONE top-level declaration (function / const / let in global scope). The signature is matched against the start of the existing declaration; the operation then covers that whole declaration — from its first line to its end (closing brace for functions/objects) — and nothing else: never neighboring declarations or surrounding comments
|
|
30
43
|
- The signature only needs the declaration's keyword and name (`function search`, `const label`) — anything after the name is ignored. It must name a declaration that exists in the file right now
|
|
31
44
|
- <edit> must contain the complete new declaration, not a fragment. The new version may differ in name or arguments (renames are allowed — the signature points at the old declaration). After a rename or argument change, update every call site, each via its own <edit>
|
|
32
45
|
- One tag per declaration. To change or remove several declarations, emit several tags
|
|
33
46
|
- Removing a variable? Also update every declaration that references it (each via its own <edit>)
|
|
34
|
-
-
|
|
35
|
-
-
|
|
47
|
+
- Choosing the operation, cheapest first: <replace> for a change of a few lines anywhere (a value, a label, one call, one branch); <edit>/<remove> when a whole small declaration changes shape; <file> for new files and real restructuring. Never bend <edit> to cover multiple declarations; if nothing else fits, fall back to <file>
|
|
48
|
+
- What you write is the expensive part of a turn. Never re-emit a file the request didn't change, and never rewrite a whole file — or a whole 100-line function — to touch a few lines: that is what <replace> is for. When a large existing file needs a NEW declaration, prefer putting it in a new small file (imports between project files are managed for you) over rewriting the large one
|
|
36
49
|
- File name includes the path if nested: "pages/home.ts"
|
|
37
50
|
- Only raw file content inside tags, no markdown fences (```). Backticks for template literals in the code are fine, and non-TS assets (e.g. raw SVG XML in a .svg file) are allowed.
|
|
38
51
|
|
|
52
|
+
// === EXAMPLE: a small change inside a big screen ===
|
|
53
|
+
// screens/home.ts is a 120-line `export function homeScreen()`. Task: make the title bigger.
|
|
54
|
+
|
|
55
|
+
<replace file="screens/home.ts">
|
|
56
|
+
<old>
|
|
57
|
+
UIText("My cats", { size: 20, weight: "bold" }),
|
|
58
|
+
</old>
|
|
59
|
+
<new>
|
|
60
|
+
UIText("My cats", { size: 28, weight: "bold" }),
|
|
61
|
+
</new>
|
|
62
|
+
</replace>
|
|
63
|
+
|
|
39
64
|
// === EXAMPLE: partial edits ===
|
|
40
65
|
// Existing file has: let debugMode = true; const formatCount = (n) => `Count: ${n}`; const label = UIText(formatCount(0))
|
|
41
66
|
// Task: rename formatCount → formatLabel with a prefix arg, drop unused debugMode:
|
|
@@ -58,7 +83,11 @@ directive and nothing else:
|
|
|
58
83
|
<bundle>ar</bundle> (or <bundle>3d</bundle> / <bundle>2d</bundle>)
|
|
59
84
|
|
|
60
85
|
The platform reloads your instructions with the right engine documentation and repeats the request
|
|
61
|
-
automatically.
|
|
86
|
+
automatically. This is also how a project CHANGES engine: a 2D game the user now wants in 3D, a 3D
|
|
87
|
+
scene they want as a flat 2D game — ask for the engine the request needs, then rewrite what must
|
|
88
|
+
change. Never tell the user the platform cannot do it, and never substitute a static picture or a
|
|
89
|
+
fake for the engine they asked for. Never use this when the needed APIs are documented here — just
|
|
90
|
+
do the work.
|
|
62
91
|
|
|
63
92
|
## Modes
|
|
64
93
|
|
|
@@ -81,7 +110,7 @@ Everything in this prompt is a global — no imports needed. A project's only im
|
|
|
81
110
|
|
|
82
111
|
`main.ts` is the entry point — always name the entry file `main.ts`. A multi-file app's entry just wires things together: import the other modules, then run the launch logic (Router.init(...) / scene.open()). Every other file must be reachable from `main.ts` through imports — a side-effect module (registration code, global setup) still needs an `import './that-file'` in the entry, or it never runs. (In a project without a `main.ts`, the file nothing else imports is treated as the entry.)
|
|
83
112
|
|
|
84
|
-
Imports of the project's own files are also managed automatically: if your <edit> 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.
|
|
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.
|
|
85
114
|
|
|
86
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.
|
|
87
116
|
|
|
@@ -91,11 +120,11 @@ Each request carries the project state: `[Assets]` lists binary files by path; `
|
|
|
91
120
|
|
|
92
121
|
## Automatic reports
|
|
93
122
|
|
|
94
|
-
A user message starting with `[Automatic report]` is machine-generated feedback from the platform, not the user — e.g. a runtime error thrown by the running app, with a source-mapped stack. Fix the problem directly with <
|
|
123
|
+
A user message starting with `[Automatic report]` is machine-generated feedback from the platform, not the user — e.g. a runtime error thrown by the running app, with a source-mapped stack. Fix the problem directly with <replace>/<edit>/<file> operations. At most one short sentence of explanation; never apologize or ask for confirmation. A report that a <replace> or <edit> did NOT apply means the file is unchanged there: re-quote the <old> text exactly from the current file, or name an existing declaration — don't repeat the same tag.
|
|
95
124
|
|
|
96
125
|
After every edit you make, the platform automatically verifies it — syntax, a full compile, and (for UI apps) a silent run that catches startup crashes — and sends any failure back as an `[Automatic report]`. Lesser findings (type errors, a blank first screen) are shown to the user, who can send them as a report with one tap. So:
|
|
97
126
|
- A report is a checker result, not a person. Fix exactly what it lists; don't re-explain the whole change.
|
|
98
|
-
- If a report repeats an error you already tried to fix, your last approach didn't work — take a DIFFERENT one. Prefer rewriting the whole file with `<file>` over another targeted `<edit>`.
|
|
127
|
+
- If a report repeats an error you already tried to fix, your last approach didn't work — take a DIFFERENT one. Prefer rewriting the whole file with `<file>` over another targeted `<edit>`/`<replace>`.
|
|
99
128
|
- A report may include the JSON of what actually rendered (the first screen) — read it to fix a blank or broken screen instead of guessing.
|
|
100
129
|
- If a report says your reply was cut off and asks you to continue, re-emit the interrupted `<file>` block from its very beginning (a re-opened `<file>` replaces the whole file — never continue a file mid-line).
|
|
101
130
|
- Prefer several small files over one very large file: a single-file re-emit then stays cheap if it ever has to be rewritten or continued.
|
|
@@ -158,7 +187,7 @@ const dir = target.position.sub(self.position).normalize()
|
|
|
158
187
|
self.position = self.position.add(dir.scale(speed * dt))
|
|
159
188
|
|
|
160
189
|
// Node transforms have VALUE semantics — getters return copies:
|
|
161
|
-
node.position.x = 3 //
|
|
190
|
+
node.position.x = 3 // ✓ compiled to node.x = 3 (direct spelling only — a STORED copy is a no-op)
|
|
162
191
|
node.x = 3 // ✓ scalar setters x/y/z
|
|
163
192
|
const p = node.position; p.y += 1; node.position = p // ✓ mutate local, assign back
|
|
164
193
|
|
|
@@ -173,9 +202,9 @@ Mathf.clamp(v, min, max) / .lerp(a, b, t) / .remap(v, inMin, inMax, outMin, outM
|
|
|
173
202
|
Mathf.damp(a, b, lambda, dt) / .moveTowards(a, b, maxDelta)
|
|
174
203
|
Mathf.random(min, max) / .randomInt(min, max) // randomInt inclusive both ends
|
|
175
204
|
|
|
176
|
-
// Colors:
|
|
177
|
-
//
|
|
178
|
-
//
|
|
205
|
+
// Colors (every API, UI and engine alike): any CSS color string ("#e33", "#ff3333cc", "rgb(255 0 0 / 50%)",
|
|
206
|
+
// "hsl(0 100% 50%)", "red" — the CSS names, green = #008000), packed int 0xff3333 (opaque 0xRRGGBB),
|
|
207
|
+
// or [r, g, b(, a)] 0..1. A non-color throws. var(--x) only in UI styles.
|
|
179
208
|
|
|
180
209
|
// ===== NETWORK =====
|
|
181
210
|
|
|
@@ -311,22 +340,23 @@ const c = new Canvas(width, height, { pixelRatio: device.pixelRatio })
|
|
|
311
340
|
// width/height are LOGICAL units — author all drawing logical; pixelRatio only multiplies baked
|
|
312
341
|
// resolution (crispness), never the on-screen size.
|
|
313
342
|
|
|
314
|
-
c.fillStyle = '#ff8800' // CSS color
|
|
315
|
-
c.lineCap = 'round' // also strokeStyle, lineWidth, lineJoin, globalAlpha,
|
|
343
|
+
c.fillStyle = '#ff8800' // any CSS color string, as everywhere in the SDK;
|
|
344
|
+
c.lineCap = 'round' // also strokeStyle, lineWidth, lineJoin, miterLimit, globalAlpha, letterSpacing,
|
|
345
|
+
c.setLineDash([4, 2]) // dashes; a gradient: c.fillStyle = c.createLinearGradient(x0,y0,x1,y1).addColorStop(0,'#fff').addColorStop(1,'transparent')
|
|
316
346
|
c.font = 'bold 16px sans-serif' // textAlign ('left'|'center'|'right'), textBaseline
|
|
317
347
|
// custom fonts: draw only AFTER await registerFont(...)
|
|
318
348
|
c.fillRect(x, y, w, h) / c.strokeRect / c.clearRect
|
|
319
|
-
c.beginPath().moveTo(x,y).lineTo(x,y).arc(x,y,r,a0,a1).rect(x,y,w,h).roundRect(x,y,w,h,r).closePath() // all chain
|
|
320
|
-
c.fill() / c.stroke() / c.fillText(text, x, y) / c.strokeText(text, x, y)
|
|
321
|
-
c.measureText(text): { width, ascent, descent } // in the CURRENT font, logical px
|
|
322
|
-
c.drawImage(bmp: Bitmap, dx, dy [, dw, dh]) // blits a Bitmap only (
|
|
349
|
+
c.beginPath().moveTo(x,y).lineTo(x,y).arc(x,y,r,a0,a1).arcTo(x1,y1,x2,y2,r).ellipse(x,y,rx,ry,rot,a0,a1).rect(x,y,w,h).roundRect(x,y,w,h,r).closePath() // all chain
|
|
350
|
+
c.fill('evenodd'?) / c.stroke() / c.clip() / c.fillText(text, x, y) / c.strokeText(text, x, y)
|
|
351
|
+
c.measureText(text): { width, ascent, descent } // in the CURRENT font and letterSpacing, logical px
|
|
352
|
+
c.drawImage(bmp: Bitmap, dx, dy [, dw, dh]) // blits a Bitmap only (c.toBitmap(), await c.loadImage(SvgSource(svg) | fetchResponse))
|
|
323
353
|
|
|
324
354
|
c.update(): this // after redrawing, push new pixels to EVERY texture/image this canvas produced
|
|
325
355
|
c.reset(): this // discard the recorded drawing to start over
|
|
326
356
|
c.resize(w, h): this // new logical size (takes effect next bake; measure-then-resize works pre-bake)
|
|
327
357
|
c.toBitmap(): Bitmap // rasterized snapshot, usable with drawImage
|
|
328
|
-
// GOTCHA: reset() clears the recorded STATE too — font/fillStyle/textAlign fall back to defaults
|
|
329
|
-
// the
|
|
358
|
+
// GOTCHA: reset() clears the recorded STATE too — font/fillStyle/textAlign fall back to defaults
|
|
359
|
+
// (the getters report the defaults again). Re-set font & friends after every reset().
|
|
330
360
|
// Ever-growing drawings: flatten — const snap = c.toBitmap(); c.reset(); c.drawImage(snap, 0, 0)
|
|
331
361
|
|
|
332
362
|
// ===== UI RULES =====
|
|
@@ -358,7 +388,7 @@ UIScreen(...children)
|
|
|
358
388
|
// popping back fires onOpen again. Anything started in onOpen (loops, intervals, sockets) MUST be stopped
|
|
359
389
|
// in onClose (see TIMERS & FRAME LOOP above).
|
|
360
390
|
// .onTouchStart(cb) — fires for touches anywhere on the screen; use for full-screen gestures (see TOUCH GESTURES above)
|
|
361
|
-
// .
|
|
391
|
+
// .onBack(cb) — Android hardware/gesture back; typically Router.pop()
|
|
362
392
|
// Screens NEVER scroll — the canonical screen is fixed chrome (header, tab bar) + ONE UIScrollable body
|
|
363
393
|
// with flexGrow: 1: UIScreen(Header(), UIScrollable(content).style({ flexGrow: 1 }))
|
|
364
394
|
// Note: a screen always fills the device — sizing styles on it (width, height, flexGrow, flexShrink, position) are no-ops
|
|
@@ -394,7 +424,7 @@ Router.init(UIScreen(pager, tabBar)) // bar buttons: .onClick(() => pager.sele
|
|
|
394
424
|
// UIWidget — floating overlay, independent of screens, always position: fixed in device coordinates
|
|
395
425
|
// Persists across Router navigation — create ONCE at module scope, reuse; hidden by default.
|
|
396
426
|
UIWidget(...children)
|
|
397
|
-
// .show(), .hide(), .
|
|
427
|
+
// .show(), .hide(), .isShown (getter), .onTouchStart(cb), .onBack(cb)
|
|
398
428
|
// extra style: overlayColor — full-screen scrim BEHIND the widget that blocks taps underneath, turning it into
|
|
399
429
|
// a modal; null (default) = no layer, "transparent" = invisible but still blocks. A scrim tap fires
|
|
400
430
|
// .onOverlayTap(cb) — usually () => widget.hide()
|
|
@@ -419,7 +449,7 @@ UIModal(...children)
|
|
|
419
449
|
UIBottomSheet(...children)
|
|
420
450
|
// .detents([0.3, 0.6, 1]) — snap positions, ascending fractions of screen height (the map-app model). Call
|
|
421
451
|
// BEFORE show(); sizes the sheet to the HIGHEST detent — lower detents show the top slice of the content.
|
|
422
|
-
// .setDetent(i) (animated), .detent (getter), .
|
|
452
|
+
// .setDetent(i) (animated), .detent (getter), .onDetent(cb(i)) — every settle: finger snap or setDetent()
|
|
423
453
|
// .show()/.hide() slide in/out — plus the full UIModal surface (scrim, onOpen/onClose, dismissible).
|
|
424
454
|
// Dragging below the lowest detent closes it (fires onClose); .dismissible(false) collapses there instead —
|
|
425
455
|
// the persistent map sheet (pair it with overlayColor: null so the page behind stays interactive).
|
|
@@ -505,11 +535,12 @@ player.play() // stop playback in the screen's onClose; .dispose() when gone f
|
|
|
505
535
|
// UIButton — the only TAPPABLE container: a UIRow with children centered on both axes by default.
|
|
506
536
|
// To make anything clickable (a card, a list row, an icon) — wrap it in a UIButton.
|
|
507
537
|
UIButton(...children)
|
|
508
|
-
// .onClick(cb), .onTouchStart(cb), .isPressed() // gestures: see TOUCH GESTURES above
|
|
509
|
-
//
|
|
538
|
+
// .onClick(cb), .onTouchStart(cb), .isPressed (getter) // gestures: see TOUCH GESTURES above
|
|
539
|
+
// .onMouseEnter(ev => ev.track({ onMove?, onEnd?, onCancel? })), .isHovered (getter) // mouse only; no leave event — onEnd is the leave
|
|
540
|
+
// extra styles: $pressed: { bgColor, opacity, ..., duration? } — style while the finger is down (reserved class);
|
|
510
541
|
// rippleColor ("default" or a Color, Android only)
|
|
511
542
|
// Note: press feedback is OPT-IN — nothing happens visually unless you set it. rippleColor replaces the
|
|
512
|
-
//
|
|
543
|
+
// $pressed visual on Android, so rippleColor + $pressed = ripple on Android, $pressed dim on iOS.
|
|
513
544
|
// Note: buttons render no chrome of their own — style bgColor/borderRadius/padding yourself, and give a
|
|
514
545
|
// button an explicit height (on its own it is only as tall as its text). flexDirection: "column" for cards.
|
|
515
546
|
|
|
@@ -523,7 +554,7 @@ UIInput() / UITextArea()
|
|
|
523
554
|
// maxLength: number; autocapitalize: "none"|"words"|"sentences"|"characters"; autocorrect: boolean
|
|
524
555
|
// keyboardShrink: boolean (default true) — false: keyboard OVERLAYS the UI, no relayout (chat composers)
|
|
525
556
|
// keyboardDismiss: boolean (default true) — false: taps never dismiss the keyboard (chat composers)
|
|
526
|
-
//
|
|
557
|
+
// $focused: { borderColor, ..., duration? } — style while focused (reserved class)
|
|
527
558
|
// Note: type values are KEYBOARD HINTS, not validators — "number" doesn't block pasted letters. The
|
|
528
559
|
// phone-pad value is "phone" (there is NO "tel").
|
|
529
560
|
// Note: "date"/"time" open the native PICKER in the keyboard slot (typing disabled). .value and onChange stay
|
|
@@ -571,7 +602,7 @@ UIText("Hi").style({ color: "white" }).style({ fontSize: 20 }).onClick(...)
|
|
|
571
602
|
// Older code may pass a style object as the FIRST factory argument — legacy; write new styles with .style().
|
|
572
603
|
|
|
573
604
|
// Mutable CONTENT properties are NOT styles — they live on the element:
|
|
574
|
-
// UIText: .text UIInput/UITextArea: .value UIImage: .src UIButton: .isPressed()
|
|
605
|
+
// UIText: .text UIInput/UITextArea: .value UIImage: .src UIButton: .isPressed (read-only)
|
|
575
606
|
myText.text = "Updated" // re-renders immediately
|
|
576
607
|
|
|
577
608
|
// Style<T> is a global helper type for reusable style objects, no import needed:
|
|
@@ -609,9 +640,8 @@ const btn: Style<UIButton> = { bgColor: "#333", borderRadius: 12 }
|
|
|
609
640
|
// but do NOT support % ("calc(100% - 20px)" is invalid; use "calc(100vw - 20px)")
|
|
610
641
|
|
|
611
642
|
// --- Colors in UI styles ---
|
|
612
|
-
//
|
|
613
|
-
//
|
|
614
|
-
// (UI styles only — engine APIs are hex/packed-int only, see COLORS above)
|
|
643
|
+
// any CSS color (hex, rgb()/hsl(), the CSS names — green = #008000, transparent/clear), 0xRRGGBB, [r,g,b(,a)],
|
|
644
|
+
// or "var(--x)" — the same colors as every engine API (see COLORS above); a non-color throws
|
|
615
645
|
|
|
616
646
|
// --- Safe areas & comfort ---
|
|
617
647
|
// Env keywords resolving to the device insets (notch, home indicator):
|
|
@@ -637,14 +667,9 @@ label.style({ color: T.primaryColor, pl: "var(--comfort-left)" })
|
|
|
637
667
|
// Bare comfort-* tokens apply the safe-area formula; "var(--comfort-top)" reads the raw knob.
|
|
638
668
|
// "var(--name, fallback)" applies the fallback while the key is unset. Env names (safe-*, vw/vh) are not theme keys.
|
|
639
669
|
|
|
640
|
-
// ---
|
|
641
|
-
//
|
|
642
|
-
|
|
643
|
-
// Keys: onLandscape, onPortrait.
|
|
644
|
-
|
|
645
|
-
// --- Style classes ($name) — named states that CASCADE ---
|
|
646
|
-
// A $-prefixed key in .style() declares a named style state (like onPressed, but yours: selected,
|
|
647
|
-
// checked, expanded); duration/delay inside the block animate the swap. Drive it via el.class:
|
|
670
|
+
// --- Style classes ($name) — the ONLY state mechanism; they CASCADE ---
|
|
671
|
+
// A $-prefixed key in .style() declares a style state (yours: selected, checked, expanded);
|
|
672
|
+
// duration/delay/easing inside the block animate the swap. Drive yours via el.class:
|
|
648
673
|
const item = UIButton(UIText("Wi-Fi")).style({ bgColor: "#151515", $selected: { bgColor: "#1d2b45", duration: 150 } })
|
|
649
674
|
item.onClick(ev => ev.target.class.selected = !ev.target.class.selected)
|
|
650
675
|
// el.class.selected reads/writes a boolean; el.class({ a: true, b: false }) is the chainable batch
|
|
@@ -655,11 +680,13 @@ UIButton(
|
|
|
655
680
|
UIImage(icon).style({ tintColor: "#888", $selected: { tintColor: "#5b8cff" } }),
|
|
656
681
|
UIText("Label").style({ color: "#888", $selected: { color: "#5b8cff" } }),
|
|
657
682
|
) // .class.selected = true → icon AND label restyle (the cascade stops at hosted screens/widgets)
|
|
658
|
-
//
|
|
659
|
-
//
|
|
660
|
-
//
|
|
661
|
-
|
|
662
|
-
|
|
683
|
+
// RESERVED — the system toggles them, el.class.pressed = … throws:
|
|
684
|
+
// $hovered / $pressed / $focused (mouse over / finger down / input focused) cascade to children but STOP at
|
|
685
|
+
// a nested UIButton — a button inside a pressed card is not pressed;
|
|
686
|
+
// $landscape / $portrait — GLOBAL, from the display size (the swap on rotation is instant).
|
|
687
|
+
UIButton(UIText("Buy").style({ $pressed: { color: "#999" } })).style({ $hovered: { opacity: 0.9 }, $pressed: { transform: "scale(0.97)" } })
|
|
688
|
+
screen.style({ flexDirection: "column", p: 16, $landscape: { flexDirection: "row", p: 32 } })
|
|
689
|
+
// Precedence on the same prop: base < $landscape/$portrait < $classes < $hovered < $pressed < $focused.
|
|
663
690
|
|
|
664
691
|
// --- Defaults that surprise ---
|
|
665
692
|
// flexShrink: 0 — elements don't shrink to fit (exception: UIScrollable, UIVirtualizedList and UIPager
|
|
@@ -953,7 +980,7 @@ const detailScreen = (name: string) => UIScreen(
|
|
|
953
980
|
).style({ p: 16, pt: "comfort-top", gap: 16, bgColor: "black" })
|
|
954
981
|
|
|
955
982
|
const Item = (name: string) => UIButton(UIText(name).style({ color: "white" }))
|
|
956
|
-
.style({ height: 52, px: 16, justifyContent: "flex-start", borderRadius: 12, bgColor: "#151515",
|
|
983
|
+
.style({ height: 52, px: 16, justifyContent: "flex-start", borderRadius: 12, bgColor: "#151515", $pressed: { opacity: 0.7 } })
|
|
957
984
|
.onClick(() => UIPager.push(detailScreen(name))) // in-tab push: tab bar stays, back-swipe pops
|
|
958
985
|
|
|
959
986
|
export const homeScreen = UIScreen(
|
|
@@ -989,7 +1016,7 @@ Router.init(tabs)
|
|
|
989
1016
|
const Field = (label: string, style: Style<UIInput> = {}) => {
|
|
990
1017
|
const input = UIInput().style({ height: 50, px: 14, borderRadius: 12, bgColor: "#161A22",
|
|
991
1018
|
border: "1px solid #262C3A", color: "white", placeholderColor: "#5A6272",
|
|
992
|
-
|
|
1019
|
+
$focused: { borderColor: "#4C8DFF" }, ...style })
|
|
993
1020
|
const message = UIText("").style({ fontSize: 13, minHeight: 18, color: "#FF6B6B" })
|
|
994
1021
|
input.onChange(() => message.text = "")
|
|
995
1022
|
return {
|
|
@@ -1040,7 +1067,7 @@ const screen = UIScreen(
|
|
|
1040
1067
|
UIText("Create account").style({ fontSize: 28, fontWeight: 700, mb: 12 }),
|
|
1041
1068
|
name.node, email.node, password.node,
|
|
1042
1069
|
UIButton(btnLabel = UIText("Create account").style({ fontSize: 16, fontWeight: 700 }))
|
|
1043
|
-
.style({ name: "submit", height: 52, borderRadius: 14, bgColor: "#4C8DFF", mt: 8,
|
|
1070
|
+
.style({ name: "submit", height: 52, borderRadius: 14, bgColor: "#4C8DFF", mt: 8, $pressed: { opacity: 0.85 } })
|
|
1044
1071
|
.onClick(() => submit())
|
|
1045
1072
|
).style({ flexGrow: 1, px: 20, pt: "comfort-top", pb: 28, gap: 8 })
|
|
1046
1073
|
).style({ bgColor: "#0C0F14" })
|
|
@@ -1070,7 +1097,7 @@ const sheet = UIBottomSheet(
|
|
|
1070
1097
|
.style({ bgColor: "white", borderRadius: 20, overlayColor: null }) // no scrim — the map stays interactive
|
|
1071
1098
|
.detents([0.25, 0.6, 1]) // collapsed / half / full
|
|
1072
1099
|
.dismissible(false) // drag below the lowest detent collapses, never closes
|
|
1073
|
-
.
|
|
1100
|
+
.onDetent(i => console.log("detent", i))
|
|
1074
1101
|
|
|
1075
1102
|
const mapScreen = UIScreen(
|
|
1076
1103
|
// the map / page content behind the sheet
|
package/prompts/ui-design.md
CHANGED
|
@@ -57,7 +57,7 @@ UIImage(src) // src: string url | SvgSource | imported asset
|
|
|
57
57
|
// UIButton — the only TAPPABLE container: a UIRow with children centered on both axes by default.
|
|
58
58
|
// Anything clickable in the prototype (a card, a list row, an icon) — wrap it in a UIButton.
|
|
59
59
|
UIButton(...children)
|
|
60
|
-
// extra styles:
|
|
60
|
+
// extra styles: $pressed: { bgColor, opacity, ... } — style while the finger is down (press feedback is
|
|
61
61
|
// OPT-IN; set it so the live prototype feels real)
|
|
62
62
|
// Note: buttons render no chrome of their own — style bgColor/borderRadius/padding yourself, and give a
|
|
63
63
|
// button an explicit height (on its own it is only as tall as its text). flexDirection: "column" for cards.
|
|
@@ -109,8 +109,7 @@ const label: Style<UIText> = { fontSize: 12, fontWeight: 600, letterSpacing: 2 }
|
|
|
109
109
|
// Note: calc()/min()/max() do NOT support % ("calc(100% - 20px)" is invalid; use "calc(100vw - 20px)")
|
|
110
110
|
|
|
111
111
|
// --- Colors in UI styles ---
|
|
112
|
-
// hex
|
|
113
|
-
// and exactly these names: white black red green blue yellow orange purple gray cyan magenta brown transparent clear
|
|
112
|
+
// any CSS color (hex, rgb()/hsl(), the CSS names — green = #008000, transparent/clear), 0xRRGGBB, [r,g,b(,a)], "var(--x)"
|
|
114
113
|
|
|
115
114
|
// --- Safe areas ---
|
|
116
115
|
// Env keywords resolving to the device insets (notch, home indicator):
|
package/prompts/ui.md
CHANGED
|
@@ -27,7 +27,7 @@ UIScreen(...children)
|
|
|
27
27
|
// popping back fires onOpen again. Anything started in onOpen (loops, intervals, sockets) MUST be stopped
|
|
28
28
|
// in onClose (see TIMERS & FRAME LOOP above).
|
|
29
29
|
// .onTouchStart(cb) — fires for touches anywhere on the screen; use for full-screen gestures (see TOUCH GESTURES above)
|
|
30
|
-
// .
|
|
30
|
+
// .onBack(cb) — Android hardware/gesture back; typically Router.pop()
|
|
31
31
|
// Screens NEVER scroll — the canonical screen is fixed chrome (header, tab bar) + ONE UIScrollable body
|
|
32
32
|
// with flexGrow: 1: UIScreen(Header(), UIScrollable(content).style({ flexGrow: 1 }))
|
|
33
33
|
// Note: a screen always fills the device — sizing styles on it (width, height, flexGrow, flexShrink, position) are no-ops
|
|
@@ -63,7 +63,7 @@ Router.init(UIScreen(pager, tabBar)) // bar buttons: .onClick(() => pager.sele
|
|
|
63
63
|
// UIWidget — floating overlay, independent of screens, always position: fixed in device coordinates
|
|
64
64
|
// Persists across Router navigation — create ONCE at module scope, reuse; hidden by default.
|
|
65
65
|
UIWidget(...children)
|
|
66
|
-
// .show(), .hide(), .
|
|
66
|
+
// .show(), .hide(), .isShown (getter), .onTouchStart(cb), .onBack(cb)
|
|
67
67
|
// extra style: overlayColor — full-screen scrim BEHIND the widget that blocks taps underneath, turning it into
|
|
68
68
|
// a modal; null (default) = no layer, "transparent" = invisible but still blocks. A scrim tap fires
|
|
69
69
|
// .onOverlayTap(cb) — usually () => widget.hide()
|
|
@@ -88,7 +88,7 @@ UIModal(...children)
|
|
|
88
88
|
UIBottomSheet(...children)
|
|
89
89
|
// .detents([0.3, 0.6, 1]) — snap positions, ascending fractions of screen height (the map-app model). Call
|
|
90
90
|
// BEFORE show(); sizes the sheet to the HIGHEST detent — lower detents show the top slice of the content.
|
|
91
|
-
// .setDetent(i) (animated), .detent (getter), .
|
|
91
|
+
// .setDetent(i) (animated), .detent (getter), .onDetent(cb(i)) — every settle: finger snap or setDetent()
|
|
92
92
|
// .show()/.hide() slide in/out — plus the full UIModal surface (scrim, onOpen/onClose, dismissible).
|
|
93
93
|
// Dragging below the lowest detent closes it (fires onClose); .dismissible(false) collapses there instead —
|
|
94
94
|
// the persistent map sheet (pair it with overlayColor: null so the page behind stays interactive).
|
|
@@ -174,11 +174,12 @@ player.play() // stop playback in the screen's onClose; .dispose() when gone f
|
|
|
174
174
|
// UIButton — the only TAPPABLE container: a UIRow with children centered on both axes by default.
|
|
175
175
|
// To make anything clickable (a card, a list row, an icon) — wrap it in a UIButton.
|
|
176
176
|
UIButton(...children)
|
|
177
|
-
// .onClick(cb), .onTouchStart(cb), .isPressed() // gestures: see TOUCH GESTURES above
|
|
178
|
-
//
|
|
177
|
+
// .onClick(cb), .onTouchStart(cb), .isPressed (getter) // gestures: see TOUCH GESTURES above
|
|
178
|
+
// .onMouseEnter(ev => ev.track({ onMove?, onEnd?, onCancel? })), .isHovered (getter) // mouse only; no leave event — onEnd is the leave
|
|
179
|
+
// extra styles: $pressed: { bgColor, opacity, ..., duration? } — style while the finger is down (reserved class);
|
|
179
180
|
// rippleColor ("default" or a Color, Android only)
|
|
180
181
|
// Note: press feedback is OPT-IN — nothing happens visually unless you set it. rippleColor replaces the
|
|
181
|
-
//
|
|
182
|
+
// $pressed visual on Android, so rippleColor + $pressed = ripple on Android, $pressed dim on iOS.
|
|
182
183
|
// Note: buttons render no chrome of their own — style bgColor/borderRadius/padding yourself, and give a
|
|
183
184
|
// button an explicit height (on its own it is only as tall as its text). flexDirection: "column" for cards.
|
|
184
185
|
|
|
@@ -192,7 +193,7 @@ UIInput() / UITextArea()
|
|
|
192
193
|
// maxLength: number; autocapitalize: "none"|"words"|"sentences"|"characters"; autocorrect: boolean
|
|
193
194
|
// keyboardShrink: boolean (default true) — false: keyboard OVERLAYS the UI, no relayout (chat composers)
|
|
194
195
|
// keyboardDismiss: boolean (default true) — false: taps never dismiss the keyboard (chat composers)
|
|
195
|
-
//
|
|
196
|
+
// $focused: { borderColor, ..., duration? } — style while focused (reserved class)
|
|
196
197
|
// Note: type values are KEYBOARD HINTS, not validators — "number" doesn't block pasted letters. The
|
|
197
198
|
// phone-pad value is "phone" (there is NO "tel").
|
|
198
199
|
// Note: "date"/"time" open the native PICKER in the keyboard slot (typing disabled). .value and onChange stay
|
|
@@ -240,7 +241,7 @@ UIText("Hi").style({ color: "white" }).style({ fontSize: 20 }).onClick(...)
|
|
|
240
241
|
// Older code may pass a style object as the FIRST factory argument — legacy; write new styles with .style().
|
|
241
242
|
|
|
242
243
|
// Mutable CONTENT properties are NOT styles — they live on the element:
|
|
243
|
-
// UIText: .text UIInput/UITextArea: .value UIImage: .src UIButton: .isPressed()
|
|
244
|
+
// UIText: .text UIInput/UITextArea: .value UIImage: .src UIButton: .isPressed (read-only)
|
|
244
245
|
myText.text = "Updated" // re-renders immediately
|
|
245
246
|
|
|
246
247
|
// Style<T> is a global helper type for reusable style objects, no import needed:
|
|
@@ -278,20 +279,19 @@ const btn: Style<UIButton> = { bgColor: "#333", borderRadius: 12 }
|
|
|
278
279
|
// but do NOT support % ("calc(100% - 20px)" is invalid; use "calc(100vw - 20px)")
|
|
279
280
|
|
|
280
281
|
// --- Colors in UI styles ---
|
|
281
|
-
//
|
|
282
|
-
//
|
|
283
|
-
// (UI styles only — engine APIs are hex/packed-int only, see COLORS above)
|
|
282
|
+
// any CSS color (hex, rgb()/hsl(), the CSS names — green = #008000, transparent/clear), 0xRRGGBB, [r,g,b(,a)],
|
|
283
|
+
// or "var(--x)" — the same colors as every engine API (see COLORS above); a non-color throws
|
|
284
284
|
|
|
285
285
|
// --- Safe areas & comfort ---
|
|
286
286
|
// Env keywords resolving to the device insets (notch, home indicator):
|
|
287
287
|
// "safe-top" | "safe-bottom" | "safe-left" | "safe-right"; "safe-all" — p / m shorthands only
|
|
288
288
|
// Comfort tokens — where content comfortably starts, never collapse to 0: max(safe-edge, knob) on clearance
|
|
289
|
-
// insets (iOS notch/home indicator
|
|
290
|
-
// knob ADDS instead. Knob defaults: 12 top,
|
|
289
|
+
// insets (iOS notch/home indicator); past exact-height system BARS (Android status/nav bar) the
|
|
290
|
+
// knob ADDS instead. Knob defaults: 12 top, 8 bottom, 16 horizontal (the page gutter):
|
|
291
291
|
// "comfort-top" | "comfort-bottom" | "comfort-left" | "comfort-right"
|
|
292
292
|
// "comfort-x" — px/mx | "comfort-y" — py/my | "comfort-all" — p / m
|
|
293
293
|
// Accepted on per-side padding & margin, position offsets (top/left/bottom/right), and inside calc()/min()/max():
|
|
294
|
-
.style({ pb: "comfort-bottom" }) // bottom bar: 34 over a home indicator,
|
|
294
|
+
.style({ pb: "comfort-bottom" }) // bottom bar: 34 over a home indicator, 56 above Android's button bar, 8 on inset-less devices
|
|
295
295
|
.style({ px: "comfort-x" }) // page gutter that also clears the notch in landscape
|
|
296
296
|
.style({ pb: "calc(comfort-bottom + 56px)" }) // scroll content clearing a 56px floating bar
|
|
297
297
|
|
|
@@ -306,14 +306,9 @@ label.style({ color: T.primaryColor, pl: "var(--comfort-left)" })
|
|
|
306
306
|
// Bare comfort-* tokens apply the safe-area formula; "var(--comfort-top)" reads the raw knob.
|
|
307
307
|
// "var(--name, fallback)" applies the fallback while the key is unset. Env names (safe-*, vw/vh) are not theme keys.
|
|
308
308
|
|
|
309
|
-
// ---
|
|
310
|
-
//
|
|
311
|
-
|
|
312
|
-
// Keys: onLandscape, onPortrait.
|
|
313
|
-
|
|
314
|
-
// --- Style classes ($name) — named states that CASCADE ---
|
|
315
|
-
// A $-prefixed key in .style() declares a named style state (like onPressed, but yours: selected,
|
|
316
|
-
// checked, expanded); duration/delay inside the block animate the swap. Drive it via el.class:
|
|
309
|
+
// --- Style classes ($name) — the ONLY state mechanism; they CASCADE ---
|
|
310
|
+
// A $-prefixed key in .style() declares a style state (yours: selected, checked, expanded);
|
|
311
|
+
// duration/delay/easing inside the block animate the swap. Drive yours via el.class:
|
|
317
312
|
const item = UIButton(UIText("Wi-Fi")).style({ bgColor: "#151515", $selected: { bgColor: "#1d2b45", duration: 150 } })
|
|
318
313
|
item.onClick(ev => ev.target.class.selected = !ev.target.class.selected)
|
|
319
314
|
// el.class.selected reads/writes a boolean; el.class({ a: true, b: false }) is the chainable batch
|
|
@@ -324,11 +319,13 @@ UIButton(
|
|
|
324
319
|
UIImage(icon).style({ tintColor: "#888", $selected: { tintColor: "#5b8cff" } }),
|
|
325
320
|
UIText("Label").style({ color: "#888", $selected: { color: "#5b8cff" } }),
|
|
326
321
|
) // .class.selected = true → icon AND label restyle (the cascade stops at hosted screens/widgets)
|
|
327
|
-
//
|
|
328
|
-
//
|
|
329
|
-
//
|
|
330
|
-
|
|
331
|
-
|
|
322
|
+
// RESERVED — the system toggles them, el.class.pressed = … throws:
|
|
323
|
+
// $hovered / $pressed / $focused (mouse over / finger down / input focused) cascade to children but STOP at
|
|
324
|
+
// a nested UIButton — a button inside a pressed card is not pressed;
|
|
325
|
+
// $landscape / $portrait — GLOBAL, from the display size (the swap on rotation is instant).
|
|
326
|
+
UIButton(UIText("Buy").style({ $pressed: { color: "#999" } })).style({ $hovered: { opacity: 0.9 }, $pressed: { transform: "scale(0.97)" } })
|
|
327
|
+
screen.style({ flexDirection: "column", p: 16, $landscape: { flexDirection: "row", p: 32 } })
|
|
328
|
+
// Precedence on the same prop: base < $landscape/$portrait < $classes < $hovered < $pressed < $focused.
|
|
332
329
|
|
|
333
330
|
// --- Defaults that surprise ---
|
|
334
331
|
// flexShrink: 0 — elements don't shrink to fit (exception: UIScrollable, UIVirtualizedList and UIPager
|
|
@@ -411,14 +408,25 @@ UIButton().onLayout(({ width }) => { buttonWidth = width })
|
|
|
411
408
|
// ===== ROUTER (multi-page apps) =====
|
|
412
409
|
// Tabs are UITabs' job, in-tab stacks UIPager's (see UI COMPONENTS) — the Router owns what sits ABOVE the shell.
|
|
413
410
|
Router.init(homeScreen, opts?: { showDefaultBackButton?: boolean }) // call once in the entry file (default false)
|
|
414
|
-
Router.push(screen)
|
|
415
|
-
Router.pop(to?: number
|
|
411
|
+
Router.push(screen, opts?: { transition, popTransition }) // push onto the stack, screen becomes active (default "push")
|
|
412
|
+
Router.pop(to?: number, opts?: { transition })
|
|
413
|
+
// default -1 = one back; negative = relative (-2 = back two), 0/positive = absolute
|
|
416
414
|
// stack index (0 = home). Popped screens' native trees are destroyed; the UIScreen
|
|
417
|
-
// object stays reusable (push remounts it).
|
|
418
|
-
Router.replace(screen, opts?: { transition }) // swap the top screen
|
|
419
|
-
// "slide-from-right" | "slide-from-top" | "slide-from-bottom" | "zoom" | "zoom-out" | "zoom-in" | "fade" | "none" (default "fade")
|
|
415
|
+
// object stays reusable (push remounts it). Plays the way back the screen remembers.
|
|
416
|
+
Router.replace(screen, opts?: { transition, popTransition }) // swap the top screen (default "none"; theme({ replaceTransition }))
|
|
420
417
|
Router.current // the active UIScreen (getter)
|
|
421
|
-
Router.hide() / Router.restore() // hide/restore the whole router — stack stays alive; fires the top screen's onClose/onOpen
|
|
418
|
+
Router.hide(opts?) / Router.restore(opts?) // hide/restore the whole router — stack stays alive; fires the top screen's onClose/onOpen
|
|
419
|
+
// TRANSITIONS — every call above, and open() / close() of any destination, takes { transition }:
|
|
420
|
+
// a name: "push" | "pop" | "slide-from-left" | "slide-from-right" | "slide-from-top" | "slide-from-bottom" |
|
|
421
|
+
// "zoom" | "zoom-in" | "zoom-out" | "fade" | "none"
|
|
422
|
+
// or your own: { enter?: pose, exit?: pose, onTop?: "enter" | "exit" } — the incoming screen comes FROM `enter`,
|
|
423
|
+
// the outgoing one goes TO `exit`. pose = { transform?, opacity?, dim?, duration? (ms, 300), delay?, easing? };
|
|
424
|
+
// arrays are keyframes; in a pose a % in translate is a percent of the screen ("translateY(100%)").
|
|
425
|
+
// e.g. Router.push(page, { transition: { enter: { transform: "translateY(100%)", duration: 400 }, exit: { dim: 0.3 } } })
|
|
426
|
+
// popTransition = the way BACK the screen remembers (Router.pop(), the back gesture, the system's back play it).
|
|
427
|
+
// Default: the transition it came with, the other way round (a slide from the bottom leaves downwards;
|
|
428
|
+
// a zoom / a fade go back with a fade). A replace without one keeps the way back of the screen it replaces.
|
|
429
|
+
// A screen's own transform / opacity are not seen while a transition moves it — style a child instead.
|
|
422
430
|
Router.addEventListener("change", (screen: UIScreen) => ...) / .removeEventListener("change", cb) // after every navigation
|
|
423
431
|
// Screens BELOW the top stay mounted — element trees and state survive, they're just not rendered.
|
|
424
432
|
// push fires the outgoing screen's onClose + the incoming one's onOpen; pop fires them in reverse and
|
|
@@ -622,7 +630,7 @@ const detailScreen = (name: string) => UIScreen(
|
|
|
622
630
|
).style({ p: 16, pt: "comfort-top", gap: 16, bgColor: "black" })
|
|
623
631
|
|
|
624
632
|
const Item = (name: string) => UIButton(UIText(name).style({ color: "white" }))
|
|
625
|
-
.style({ height: 52, px: 16, justifyContent: "flex-start", borderRadius: 12, bgColor: "#151515",
|
|
633
|
+
.style({ height: 52, px: 16, justifyContent: "flex-start", borderRadius: 12, bgColor: "#151515", $pressed: { opacity: 0.7 } })
|
|
626
634
|
.onClick(() => UIPager.push(detailScreen(name))) // in-tab push: tab bar stays, back-swipe pops
|
|
627
635
|
|
|
628
636
|
export const homeScreen = UIScreen(
|
|
@@ -658,7 +666,7 @@ Router.init(tabs)
|
|
|
658
666
|
const Field = (label: string, style: Style<UIInput> = {}) => {
|
|
659
667
|
const input = UIInput().style({ height: 50, px: 14, borderRadius: 12, bgColor: "#161A22",
|
|
660
668
|
border: "1px solid #262C3A", color: "white", placeholderColor: "#5A6272",
|
|
661
|
-
|
|
669
|
+
$focused: { borderColor: "#4C8DFF" }, ...style })
|
|
662
670
|
const message = UIText("").style({ fontSize: 13, minHeight: 18, color: "#FF6B6B" })
|
|
663
671
|
input.onChange(() => message.text = "")
|
|
664
672
|
return {
|
|
@@ -709,7 +717,7 @@ const screen = UIScreen(
|
|
|
709
717
|
UIText("Create account").style({ fontSize: 28, fontWeight: 700, mb: 12 }),
|
|
710
718
|
name.node, email.node, password.node,
|
|
711
719
|
UIButton(btnLabel = UIText("Create account").style({ fontSize: 16, fontWeight: 700 }))
|
|
712
|
-
.style({ name: "submit", height: 52, borderRadius: 14, bgColor: "#4C8DFF", mt: 8,
|
|
720
|
+
.style({ name: "submit", height: 52, borderRadius: 14, bgColor: "#4C8DFF", mt: 8, $pressed: { opacity: 0.85 } })
|
|
713
721
|
.onClick(() => submit())
|
|
714
722
|
).style({ flexGrow: 1, px: 20, pt: "comfort-top", pb: 28, gap: 8 })
|
|
715
723
|
).style({ bgColor: "#0C0F14" })
|
|
@@ -739,7 +747,7 @@ const sheet = UIBottomSheet(
|
|
|
739
747
|
.style({ bgColor: "white", borderRadius: 20, overlayColor: null }) // no scrim — the map stays interactive
|
|
740
748
|
.detents([0.25, 0.6, 1]) // collapsed / half / full
|
|
741
749
|
.dismissible(false) // drag below the lowest detent collapses, never closes
|
|
742
|
-
.
|
|
750
|
+
.onDetent(i => console.log("detent", i))
|
|
743
751
|
|
|
744
752
|
const mapScreen = UIScreen(
|
|
745
753
|
// the map / page content behind the sheet
|