lecodes-sdk 1.2.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 +8 -0
- package/dist/types/gl/Lightmap.d.ts +13 -2
- package/dist/types/gl/Material.d.ts +17 -3
- package/dist/types/gl/Model.d.ts +6 -2
- package/dist/types/gl/Node.d.ts +3 -6
- package/dist/types/gl/Scene.d.ts +41 -13
- package/dist/types/gl/Texture.d.ts +1 -1
- package/dist/types/gl/animation/Locomotion.d.ts +8 -1
- package/dist/types/inject.d.ts +11 -11
- package/dist/types/inject.editor.d.ts +1 -0
- package/dist/types/net/core.d.ts +7 -0
- package/dist/types/plugin.d.ts +76 -0
- package/dist/types/plugins/gen/camera/sdk/camera.d.ts +24 -0
- package/dist/types/plugins/gen/camera/sdk/camera.gen.d.ts +25 -0
- package/dist/types/plugins/{geolocation.d.ts → gen/geolocation/sdk/geolocation.d.ts} +2 -20
- package/dist/types/plugins/gen/geolocation/sdk/geolocation.gen.d.ts +31 -0
- package/dist/types/plugins/{map.d.ts → gen/map/sdk/map.d.ts} +9 -50
- package/dist/types/plugins/gen/map/sdk/map.gen.d.ts +53 -0
- package/dist/types/plugins/gen/push/sdk/push.d.ts +23 -0
- package/dist/types/plugins/gen/push/sdk/push.gen.d.ts +35 -0
- package/dist/types/plugins/{qr.d.ts → gen/qr-scanner/sdk/qr-scanner.d.ts} +2 -3
- package/dist/types/plugins/gen/qr-scanner/sdk/qr-scanner.gen.d.ts +15 -0
- package/dist/types/runtime/app.d.ts +9 -2
- package/dist/types/runtime/fetch.d.ts +2 -0
- package/dist/types/runtime/input.d.ts +1 -1
- package/dist/types/runtime/media.d.ts +6 -10
- package/dist/types/runtime/misc.d.ts +4 -1
- package/dist/types/runtime/net.d.ts +3 -2
- package/dist/types/runtime/touch.d.ts +32 -0
- package/dist/types/scene/defineScene.d.ts +43 -2
- package/dist/types/scene/editor.d.ts +52 -0
- package/dist/types/scene/gizmos.d.ts +7 -4
- package/dist/types/ui/NativeView.d.ts +6 -4
- package/dist/types/ui/UI.d.ts +1 -1
- package/dist/types/ui/UIBottomSheet.d.ts +6 -12
- package/dist/types/ui/UIButton.d.ts +12 -14
- package/dist/types/ui/UIContainer.d.ts +0 -6
- package/dist/types/ui/UIImage.d.ts +1 -4
- package/dist/types/ui/UIInput.d.ts +8 -24
- package/dist/types/ui/UIModal.d.ts +0 -2
- package/dist/types/ui/UINode.d.ts +70 -56
- package/dist/types/ui/UIPager.d.ts +28 -27
- package/dist/types/ui/UIPopover.d.ts +0 -2
- package/dist/types/ui/UIScreen.d.ts +12 -17
- package/dist/types/ui/UIScrollable.d.ts +1 -4
- package/dist/types/ui/UIText.d.ts +0 -2
- package/dist/types/ui/UIVideo.d.ts +3 -5
- package/dist/types/ui/UIVirtualizedList.d.ts +14 -16
- package/dist/types/ui/UIWidget.d.ts +10 -9
- package/dist/types/ui/colorKeys.gen.d.ts +9 -0
- package/dist/types/ui/presentable.d.ts +46 -32
- package/dist/types/ui/router.d.ts +18 -7
- package/dist/types/ui/styleColor.d.ts +1 -0
- package/dist/types/ui/transitions.d.ts +18 -0
- package/dist/types/ui/tree.d.ts +75 -0
- package/dist/types/version.d.ts +10 -0
- package/dist/types.json +1 -1
- package/package.json +12 -3
- package/prompts/2d.md +2 -6
- package/prompts/3d.md +1 -5
- package/prompts/README.md +1 -1
- package/prompts/canvas.md +9 -8
- package/prompts/compose.ts +1 -1
- package/prompts/core.md +3 -3
- package/prompts/design.md +1 -1
- package/prompts/dist/2d-game.md +473 -239
- package/prompts/dist/3d-app.md +553 -205
- package/prompts/dist/ar-app.md +435 -202
- package/prompts/dist/design.md +113 -95
- package/prompts/dist/ui-app.md +386 -170
- package/prompts/ui-design.md +2 -3
- package/prompts/ui.md +45 -37
- package/src/animate/tween/Animation.ts +34 -150
- package/src/animate/tween/Timeline.ts +175 -175
- package/src/animate/tween/animateValue.ts +6 -3
- package/src/animate/tween/easing.ts +10 -3
- package/src/animate/tween/spec.ts +41 -15
- package/src/audio/Sound.ts +3 -3
- package/src/audio/audio.ts +2 -1
- package/src/bridges/2d.d.ts +317 -0
- package/src/bridges/app.d.ts +91 -0
- package/src/bridges/audio.d.ts +97 -0
- package/src/bridges/canvas.d.ts +79 -0
- package/src/bridges/device.d.ts +72 -0
- package/src/bridges/fetch.d.ts +80 -0
- package/src/bridges/files.d.ts +70 -0
- package/src/bridges/gl.d.ts +1133 -0
- package/src/bridges/input.d.ts +72 -0
- package/src/bridges/media.d.ts +55 -0
- package/src/bridges/nav.d.ts +71 -0
- package/src/bridges/net.d.ts +52 -0
- package/src/bridges/service.d.ts +52 -0
- package/src/bridges/socket.d.ts +31 -0
- package/src/bridges/storage.d.ts +33 -0
- package/src/bridges/tree.d.ts +301 -0
- package/src/bridges/types.d.ts +49 -0
- package/src/canvas/Canvas.ts +114 -159
- package/src/canvas/gen/cssColor.ts +224 -0
- package/src/canvas/gen/recorder.ts +212 -0
- package/src/canvas/gen/spec.ts +201 -0
- package/src/chisel.ts +193 -0
- package/src/compile/assetMacro.ts +1 -1
- package/src/compile/bundler.ts +11 -2
- package/src/compile/compileProject.ts +43 -4
- package/src/compile/fontMacro.ts +3 -4
- package/src/compile/header.ts +26 -5
- package/src/compile/index.ts +3 -1
- package/src/compile/liteMaterial.ts +1 -1
- package/src/compile/sceneEditor.ts +11 -26
- package/src/core/color.ts +73 -30
- package/src/core/pins.ts +51 -0
- package/src/core/signals.ts +8 -1
- package/src/g2/CharacterController2D.ts +3 -3
- package/src/g2/Node2D.ts +57 -39
- package/src/g2/Physics2D.ts +2 -2
- package/src/g2/Scene2D.ts +35 -23
- package/src/g2/Texture2D.ts +1 -1
- package/src/g2/loop.ts +4 -4
- package/src/gl/CameraPlace.ts +52 -52
- package/src/gl/Foliage.ts +72 -17
- package/src/gl/Geometry.ts +1 -2
- package/src/gl/Light.ts +10 -0
- package/src/gl/Lightmap.ts +45 -29
- package/src/gl/Material.ts +95 -51
- package/src/gl/Mesh.ts +120 -120
- package/src/gl/Model.ts +21 -16
- package/src/gl/Node.ts +91 -24
- package/src/gl/Particles.ts +1 -1
- package/src/gl/Scene.ts +100 -44
- package/src/gl/Texture.ts +8 -7
- package/src/gl/animation/AnimationClip.ts +1 -1
- package/src/gl/animation/DynamicBone.ts +482 -482
- package/src/gl/animation/Locomotion.ts +8 -3
- package/src/gl/nav/NavMesh.ts +3 -4
- package/src/gl/physics/Physics.ts +2 -2
- package/src/gl/physics/physicsEvents.ts +3 -3
- package/src/gl/scenarios.ts +291 -291
- package/src/gl/terrain/Terrain.ts +4 -5
- package/src/gl/touch.ts +14 -15
- package/src/host.d.ts +2 -3
- package/src/inject.editor.ts +7 -0
- package/src/inject.ts +13 -16
- package/src/net/core.ts +6 -5
- package/src/net/index.ts +1 -1
- package/src/net/replication.ts +1 -1
- package/src/plugin.ts +191 -0
- package/src/plugins/gen/camera/contract.d.ts +27 -0
- package/src/plugins/gen/camera/sdk/camera.gen.ts +46 -0
- package/src/plugins/gen/camera/sdk/camera.ts +57 -0
- package/src/plugins/gen/geolocation/contract.d.ts +50 -0
- package/src/plugins/gen/geolocation/sdk/geolocation.gen.ts +54 -0
- package/src/plugins/{geolocation.ts → gen/geolocation/sdk/geolocation.ts} +22 -43
- package/src/plugins/gen/map/contract.d.ts +144 -0
- package/src/plugins/gen/map/sdk/map.gen.ts +88 -0
- package/src/plugins/{map.ts → gen/map/sdk/map.ts} +68 -102
- package/src/plugins/gen/push/contract.d.ts +61 -0
- package/src/plugins/gen/push/sdk/push.gen.ts +60 -0
- package/src/plugins/gen/push/sdk/push.ts +105 -0
- package/src/plugins/gen/qr-scanner/contract.d.ts +16 -0
- package/src/plugins/gen/qr-scanner/sdk/qr-scanner.gen.ts +29 -0
- package/src/plugins/gen/qr-scanner/sdk/qr-scanner.ts +52 -0
- package/src/plugins/permission.ts +5 -4
- package/src/runtime/app.ts +20 -9
- package/src/runtime/appEvents.ts +5 -4
- package/src/runtime/channel.ts +18 -15
- package/src/runtime/clipboard.ts +4 -3
- package/src/runtime/datetime.ts +2 -1
- package/src/runtime/device.ts +17 -15
- package/src/runtime/fetch.ts +30 -20
- package/src/runtime/files.ts +16 -15
- package/src/runtime/input.ts +12 -10
- package/src/runtime/media.ts +50 -46
- package/src/runtime/misc.ts +7 -3
- package/src/runtime/net.ts +8 -7
- package/src/runtime/rpc.ts +1 -3
- package/src/runtime/service.ts +19 -14
- package/src/runtime/share.ts +4 -3
- package/src/runtime/storage.ts +6 -4
- package/src/runtime/touch.ts +32 -0
- package/src/scene/defineScene.ts +61 -365
- package/src/scene/editor.ts +408 -0
- package/src/scene/editorPlugins.ts +3 -3
- package/src/scene/gizmos.ts +15 -9
- package/src/server/db/marci/query.ts +1 -1
- package/src/server/host.ts +1 -1
- package/src/server/runtime.ts +1 -1
- package/src/ui/NativeView.ts +72 -25
- package/src/ui/UI.ts +3 -3
- package/src/ui/UIBottomSheet.ts +16 -17
- package/src/ui/UIButton.ts +54 -16
- package/src/ui/UIContainer.ts +0 -6
- package/src/ui/UIImage.ts +34 -30
- package/src/ui/UIInput.ts +29 -37
- package/src/ui/UIModal.ts +1 -3
- package/src/ui/UINode.ts +347 -297
- package/src/ui/UIPager.ts +93 -78
- package/src/ui/UIPopover.ts +0 -2
- package/src/ui/UIScreen.ts +41 -36
- package/src/ui/UIScrollable.ts +19 -13
- package/src/ui/UISpacer.ts +1 -1
- package/src/ui/UITabs.ts +8 -6
- package/src/ui/UIText.ts +7 -17
- package/src/ui/UIVideo.ts +30 -27
- package/src/ui/UIVirtualizedList.ts +58 -59
- package/src/ui/UIWidget.ts +38 -18
- package/src/ui/colorKeys.gen.ts +37 -0
- package/src/ui/fonts.ts +2 -2
- package/src/ui/presentable.ts +59 -42
- package/src/ui/router.ts +52 -35
- package/src/ui/styleColor.ts +56 -0
- package/src/ui/theme.ts +7 -6
- package/src/ui/transitions.ts +249 -0
- package/src/ui/tree.ts +346 -0
- package/src/version.ts +24 -0
- package/tests/helpers/engineWorld.ts +11 -0
- package/tests/helpers/fakeTree.ts +353 -0
- package/tests/helpers/hostStubs.ts +31 -0
- package/tests/helpers/index.ts +14 -0
- package/tests/helpers/memoryMarci.ts +124 -0
- package/tests/helpers/phases.ts +23 -0
- package/tests/helpers/preload.ts +18 -0
- package/tests/helpers/stubApp.ts +2 -0
- package/tests/helpers/stubDevice.ts +2 -0
- package/tests/helpers/stubFetch.ts +2 -0
- package/tests/helpers/stubInput.ts +2 -0
- package/dist/inject.js +0 -4629
- package/dist/types/core/registry.d.ts +0 -7
- package/dist/types/plugins/camera.d.ts +0 -25
- package/dist/types/plugins/push.d.ts +0 -46
- package/src/bridges.d.ts +0 -1769
- package/src/compile/__tests__/assetIconMacro.test.ts +0 -219
- package/src/compile/__tests__/assetMacro.test.ts +0 -100
- package/src/compile/__tests__/assetName.test.ts +0 -55
- package/src/compile/__tests__/compile.test.ts +0 -310
- package/src/compile/__tests__/detectEntry.test.ts +0 -151
- package/src/compile/__tests__/fontMacro.test.ts +0 -199
- package/src/compile/__tests__/serverSplit.test.ts +0 -27
- package/src/core/__tests__/stateMachine.test.ts +0 -132
- package/src/core/registry.ts +0 -23
- package/src/plugins/camera.ts +0 -81
- package/src/plugins/push.ts +0 -132
- package/src/plugins/qr.ts +0 -73
package/prompts/dist/design.md
CHANGED
|
@@ -24,20 +24,33 @@ Remove a single top-level declaration:
|
|
|
24
24
|
|
|
25
25
|
<remove file="design/shared/ui.ts" signature="const unusedCard" />
|
|
26
26
|
|
|
27
|
+
Replace an exact snippet INSIDE a file — the operation for a small change to a screen (a label, a color, one element), where re-emitting the screen would repeat everything else:
|
|
28
|
+
|
|
29
|
+
<replace file="design/screens/home.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 (`const PrimaryButton`) — 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. 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
|
-
-
|
|
34
|
-
-
|
|
46
|
+
- Choosing the operation, cheapest first: <replace> for a change of a few lines anywhere — this is how a screen is tweaked (its default export is NOT targetable by <edit>); <edit>/<remove> when a whole small declaration in `shared/` changes shape; <file> for new screens and real restructuring. Never bend <edit> to cover multiple declarations; if nothing else fits, fall back to <file>
|
|
47
|
+
- 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 screen to touch a few lines
|
|
35
48
|
- 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
|
|
36
49
|
- `design/meta.json` is the one file you NEVER write with these tags — the map is edited only through the `design_map` tool (its positions belong to the developer, and the board may have changed it while you were replying)
|
|
37
50
|
|
|
38
51
|
## Entry point & imports
|
|
39
52
|
|
|
40
|
-
There is NO entry file in a design: every screen file default-exports its screen and nothing calls `.open()` — the board runs them. The UI API (`UIScreen`, `UIText`, …) is global — no imports needed. A screen's only imports are relative paths into `../shared/`. Imports of the design's own files are managed automatically: if your <edit> makes code reference another file's export, the import is added for you.
|
|
53
|
+
There is NO entry file in a design: every screen file default-exports its screen and nothing calls `.open()` — the board runs them. The UI API (`UIScreen`, `UIText`, …) is global — no imports needed. A screen's only imports are relative paths into `../shared/`. Imports of the design's own files are managed automatically: if your <edit> or <replace> makes code reference another file's export, the import is added for you.
|
|
41
54
|
|
|
42
55
|
Reference a design asset (image, .svg) 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.
|
|
43
56
|
|
|
@@ -47,7 +60,7 @@ Each request carries the project state: `[Assets]` lists binary files by path; `
|
|
|
47
60
|
|
|
48
61
|
## Automatic reports
|
|
49
62
|
|
|
50
|
-
A user message starting with `[Automatic report]` is machine-generated feedback from the platform, not the user. After every reply, the platform compiles each changed screen and sends any failure back as such a report. Fix the problem directly with <
|
|
63
|
+
A user message starting with `[Automatic report]` is machine-generated feedback from the platform, not the user. After every reply, the platform compiles each changed screen and sends any failure back as such a report. 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.
|
|
51
64
|
- A report is a checker result, not a person. Fix exactly what it lists.
|
|
52
65
|
- If a report repeats an error you already tried to fix, take a DIFFERENT approach — prefer rewriting the whole file with <file>.
|
|
53
66
|
- 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).
|
|
@@ -56,13 +69,25 @@ A user message starting with `[Automatic report]` is machine-generated feedback
|
|
|
56
69
|
|
|
57
70
|
You may be given tools (they appear in the API request, each with its own description). `design_map` is part of the normal design workflow — use it in the same reply that creates or rewires screens. Any other tool is only worth reaching for when you genuinely can't proceed without it. Act on a tool's result and carry on; don't thank or apologise to it.
|
|
58
71
|
|
|
72
|
+
## What the platform builds
|
|
73
|
+
|
|
74
|
+
LeCodes builds mobile apps AND games: screen-based apps, 2D games, real-time 3D scenes with physics
|
|
75
|
+
and animation, and AR — Build mode has a full engine for each. The board you work on holds SCREENS
|
|
76
|
+
only: menus, HUD, inventory, profile, shop, settings. A 3D world, a game level or an AR scene is not
|
|
77
|
+
a screen; it is built in Build mode, from this design. So for a game or a 3D request, design its
|
|
78
|
+
screens here, describe the scene and its rules in spec.md, and say plainly that the scene itself
|
|
79
|
+
comes in Build mode. Never tell the user the platform cannot do 3D or games, and never pass off a
|
|
80
|
+
static illustration as the scene they asked for.
|
|
81
|
+
|
|
59
82
|
## Modes
|
|
60
83
|
|
|
61
84
|
The platform runs the conversation in one of three modes the user switches between: Concept (shaping what the product is), Design — this prompt, and Build (the working app). When a request is really another mode's job — they want working logic, real data, or to "make the app actually do X" (a design is static mockups; never fake behavior), or they want to rethink what the product is before sketching more — do the part that belongs in the design (or answer briefly) and append the directive as the LAST line of your reply:
|
|
62
85
|
|
|
63
86
|
<mode>build</mode> (or <mode>concept</mode>)
|
|
64
87
|
|
|
65
|
-
|
|
88
|
+
Separately — the handoff. When your reply completes what the user asked for (the screens exist, compile, and are registered on the map), you have no question pending, and the app itself hasn't been built yet, end with `<mode>build</mode>`: the platform shows it as a "Build the app" button that starts the build from this design. Offer it at that moment — not after every routine edit to an already-offered design.
|
|
89
|
+
|
|
90
|
+
The platform renders the directive as a button — the user decides; nothing switches by itself. At most one <mode> directive per reply, and never for work that belongs right here.
|
|
66
91
|
|
|
67
92
|
## Important
|
|
68
93
|
|
|
@@ -82,21 +107,24 @@ The platform renders it as a "Switch to …" button — the user decides; nothin
|
|
|
82
107
|
|
|
83
108
|
// ===== UI COMPONENTS =====
|
|
84
109
|
// Every element is created by a global factory function (never `new`) that takes only the element's
|
|
85
|
-
// CONTENT
|
|
86
|
-
// every configuring method returns the element itself, so construction reads as one chain.
|
|
110
|
+
// CONTENT — children as plain arguments (or text/src/...). Everything else — styles — is configured
|
|
111
|
+
// by chaining: every configuring method returns the element itself, so construction reads as one chain.
|
|
112
|
+
UIColumn(UIText("Title"), UIButton(UIText("Go")))
|
|
113
|
+
// An ARRAY argument is flattened into the children — pass items.map(Row) directly, no spread:
|
|
114
|
+
UIColumn(header, items.map(Row), footer)
|
|
87
115
|
|
|
88
116
|
// UIRow, UIColumn — containers (UIColumn stacks vertically, UIRow horizontally)
|
|
89
|
-
UIRow(children) / UIColumn(children)
|
|
117
|
+
UIRow(...children) / UIColumn(...children)
|
|
90
118
|
|
|
91
119
|
// UIScreen — root screen, always fills the device. Behaves as a UIColumn.
|
|
92
|
-
UIScreen(children)
|
|
120
|
+
UIScreen(...children)
|
|
93
121
|
// Screens NEVER scroll — one vertical flow (article, long form, feed) = fixed chrome + ONE UIScrollable body
|
|
94
|
-
// with flexGrow: 1: UIScreen(
|
|
122
|
+
// with flexGrow: 1: UIScreen(Header(), UIScrollable(content).style({ flexGrow: 1 }))
|
|
95
123
|
// Note: a screen always fills the device — sizing styles on it (width, height, flexGrow, position) are no-ops
|
|
96
124
|
// A design screen is returned from the file's default export — never call .open()
|
|
97
125
|
|
|
98
126
|
// UIWidget — floating overlay above the screen, position: fixed in device coordinates
|
|
99
|
-
UIWidget(children)
|
|
127
|
+
UIWidget(...children)
|
|
100
128
|
// .show(), .hide(), .onOverlayTap(cb)
|
|
101
129
|
// extra style: overlayColor — full-screen scrim BEHIND the widget that blocks taps underneath, turning it
|
|
102
130
|
// into a modal (dialog / bottom sheet). Size the widget box to the CONTENT only (bottom-anchor a sheet
|
|
@@ -108,7 +136,7 @@ UIWidget(children)
|
|
|
108
136
|
|
|
109
137
|
// UIScrollable — THE scroll container: a screen's scrolling body, a list under a pinned header, a
|
|
110
138
|
// horizontal chip row / carousel
|
|
111
|
-
UIScrollable(children)
|
|
139
|
+
UIScrollable(...children)
|
|
112
140
|
// extra styles: scrollDirection ("horizontal" | "vertical", default vertical), showScrollbar: boolean
|
|
113
141
|
// Note: defaults flexShrink: 1 (scrolls instead of overflowing) — wrapping ancestors still need flexShrink: 1
|
|
114
142
|
|
|
@@ -127,8 +155,8 @@ UIImage(src) // src: string url | SvgSource | imported asset
|
|
|
127
155
|
|
|
128
156
|
// UIButton — the only TAPPABLE container: a UIRow with children centered on both axes by default.
|
|
129
157
|
// Anything clickable in the prototype (a card, a list row, an icon) — wrap it in a UIButton.
|
|
130
|
-
UIButton(children
|
|
131
|
-
// extra styles:
|
|
158
|
+
UIButton(...children)
|
|
159
|
+
// extra styles: $pressed: { bgColor, opacity, ... } — style while the finger is down (press feedback is
|
|
132
160
|
// OPT-IN; set it so the live prototype feels real)
|
|
133
161
|
// Note: buttons render no chrome of their own — style bgColor/borderRadius/padding yourself, and give a
|
|
134
162
|
// button an explicit height (on its own it is only as tall as its text). flexDirection: "column" for cards.
|
|
@@ -140,7 +168,7 @@ UIButton(children?)
|
|
|
140
168
|
|
|
141
169
|
// UISpacer — flexible empty space (defaults flexGrow: 1), eats free space along the main axis.
|
|
142
170
|
// Only when plain alignment can't express it (one item pushed to the far end while the rest stay put):
|
|
143
|
-
UIRow(
|
|
171
|
+
UIRow(title, UISpacer(), closeButton)
|
|
144
172
|
// If ALL children move together, justifyContent ("space-between", "flex-end", ...) does it with no extra element.
|
|
145
173
|
|
|
146
174
|
// ===== STYLING =====
|
|
@@ -180,8 +208,7 @@ const label: Style<UIText> = { fontSize: 12, fontWeight: 600, letterSpacing: 2 }
|
|
|
180
208
|
// Note: calc()/min()/max() do NOT support % ("calc(100% - 20px)" is invalid; use "calc(100vw - 20px)")
|
|
181
209
|
|
|
182
210
|
// --- Colors in UI styles ---
|
|
183
|
-
// hex
|
|
184
|
-
// and exactly these names: white black red green blue yellow orange purple gray cyan magenta brown transparent clear
|
|
211
|
+
// any CSS color (hex, rgb()/hsl(), the CSS names — green = #008000, transparent/clear), 0xRRGGBB, [r,g,b(,a)], "var(--x)"
|
|
185
212
|
|
|
186
213
|
// --- Safe areas ---
|
|
187
214
|
// Env keywords resolving to the device insets (notch, home indicator):
|
|
@@ -197,7 +224,7 @@ const label: Style<UIText> = { fontSize: 12, fontWeight: 600, letterSpacing: 2 }
|
|
|
197
224
|
const T = theme({ primaryColor: "#0A84FF" })
|
|
198
225
|
label.style({ color: T.primaryColor }) // or "var(--primaryColor)"
|
|
199
226
|
// theme({ color, fontFamily }) drive the DEFAULT text color/font app-wide. "var(--name, fallback)"
|
|
200
|
-
// applies the fallback while the key is unset — how
|
|
227
|
+
// applies the fallback while the key is unset — how the SDK tab bar (defineTabs) stays themeable.
|
|
201
228
|
|
|
202
229
|
// --- Defaults that surprise ---
|
|
203
230
|
// flexShrink: 0 — elements don't shrink to fit (exception: UIScrollable defaults flexShrink: 1 so it scrolls
|
|
@@ -218,14 +245,14 @@ label.style({ color: T.primaryColor }) // or "var(--primaryColor)"
|
|
|
218
245
|
// ===== REUSABLE COMPONENTS =====
|
|
219
246
|
// Extract repeated UI into factory functions — they return elements you can chain on:
|
|
220
247
|
const StatusPill = (label: string, color: string, filled = false) =>
|
|
221
|
-
UIColumn(
|
|
248
|
+
UIColumn(UIText(label).style({ fontSize: 12, fontWeight: 600, color: filled ? "#05201f" : color }))
|
|
222
249
|
.style({ height: 26, px: 10, borderRadius: 999, justifyContent: "center", alignItems: "center",
|
|
223
250
|
bgColor: filled ? color : "transparent", border: filled ? undefined : `1px solid ${color}`,
|
|
224
251
|
alignSelf: "flex-start" })
|
|
225
252
|
|
|
226
253
|
// ===== CONDITIONAL CHILDREN =====
|
|
227
|
-
// null / undefined / false
|
|
228
|
-
UIColumn(
|
|
254
|
+
// A null / undefined / false child is skipped — no element, no layout slot.
|
|
255
|
+
UIColumn(header, item.unread ? dot : null, items.map(Card))
|
|
229
256
|
|
|
230
257
|
// ===== SIZING: the two axes behave differently =====
|
|
231
258
|
// MAIN axis (row → width, column → height): elements stay as small as their content — nothing grows
|
|
@@ -253,14 +280,14 @@ lineHeight: 1.5 // WRONG — number is px (=1.5px); for a m
|
|
|
253
280
|
// A wrapping container between the scrollable and the screen is missing flexShrink: 1
|
|
254
281
|
|
|
255
282
|
// ❌ empty containers as spacers to align children (web habit)
|
|
256
|
-
UIRow(
|
|
283
|
+
UIRow(UIColumn().style({ flexGrow: 1 }), label) // WRONG
|
|
257
284
|
// ✅ alignment is a CONTAINER property, not an extra element
|
|
258
|
-
UIRow(
|
|
285
|
+
UIRow(label).style({ justifyContent: "flex-end" })
|
|
259
286
|
|
|
260
287
|
// ❌ empty element as a placeholder for a conditional child
|
|
261
|
-
UIRow(
|
|
288
|
+
UIRow(isSelected ? check : UIColumn()) // WRONG
|
|
262
289
|
// ✅ null is skipped in children — no phantom element
|
|
263
|
-
UIRow(
|
|
290
|
+
UIRow(isSelected ? check : null)
|
|
264
291
|
|
|
265
292
|
// ❌ pointing UIImage / bgImage at a project file by bare path — it won't resolve to the bundled asset
|
|
266
293
|
UIImage("./photo.jpg") // WRONG (a plain string works only for remote http(s) URLs)
|
|
@@ -293,16 +320,27 @@ design/
|
|
|
293
320
|
A design that looks designed starts from a kit, not from screens. On a new design, before the first
|
|
294
321
|
screen: `shared/tokens.ts` (a real palette — surfaces, text tiers, one or two accents, status
|
|
295
322
|
colors; a spacing/radius/type scale), then `shared/ui.ts` with the components the app will clearly
|
|
296
|
-
need.
|
|
323
|
+
need. The kit outlives the design: when the app is built, it IMPORTS `shared/tokens.ts` (and the
|
|
324
|
+
presentational components of `shared/ui.ts`) unchanged — so build tokens on `theme()`, one live
|
|
325
|
+
table for design and app alike, and shape components as things worth keeping. Screens then read as
|
|
326
|
+
composition:
|
|
297
327
|
|
|
298
328
|
```ts
|
|
299
|
-
// design/shared/tokens.ts — surfaces dark-to-light, text in tiers, accents as a family
|
|
300
|
-
|
|
329
|
+
// design/shared/tokens.ts — surfaces dark-to-light, text in tiers, accents as a family.
|
|
330
|
+
// ONE theme() call registers the vars and returns the accessors ("var(--x)" strings); the system
|
|
331
|
+
// key `color` in the same table sets the app-wide default text color. Re-calling theme() with new
|
|
332
|
+
// values (dark mode, brand swap) restyles every screen — and later the built app — live.
|
|
333
|
+
const palette = {
|
|
301
334
|
bg: "#080808", card: "#161618", cardAlt: "#1d1d20", field: "#101012",
|
|
302
335
|
border: "#2a2a2e", text: "#ffffff", muted: "#8a8a93", faint: "#5a5b62",
|
|
303
336
|
accent: "#00BFC2", accentPressed: "#00999b", accentDeep: "#0c4e4f", onAccent: "#05201f",
|
|
304
337
|
success: "#30D158", danger: "#F83131", scrim: "#000000b3",
|
|
338
|
+
// theme SYSTEM keys — SDK-rendered chrome reads these. `color` = default text color everywhere;
|
|
339
|
+
// the other four brand the tab bar (defineTabs here, UITabs in the built app). Mirror the brand:
|
|
340
|
+
color: "#ffffff",
|
|
341
|
+
primaryColor: "#00BFC2", mutedColor: "#8a8a93", tabbarBg: "#161618", screenBg: "#080808",
|
|
305
342
|
}
|
|
343
|
+
export const colors: { [K in keyof typeof palette]: string } = theme(palette)
|
|
306
344
|
export const spacing = { xs: 4, sm: 8, md: 12, lg: 16, xl: 20, xxl: 24 }
|
|
307
345
|
export const radius = { sm: 8, md: 12, lg: 16, pill: 999 }
|
|
308
346
|
```
|
|
@@ -312,24 +350,24 @@ export const radius = { sm: 8, md: 12, lg: 16, pill: 999 }
|
|
|
312
350
|
import { colors, spacing, radius } from "./tokens"
|
|
313
351
|
|
|
314
352
|
export const PrimaryButton = (label: string, o: { name?: string, disabled?: boolean } = {}) =>
|
|
315
|
-
UIButton(
|
|
353
|
+
UIButton(UIText(label).style({ fontSize: 16, fontWeight: 700, color: o.disabled ? colors.muted : colors.onAccent }))
|
|
316
354
|
.style({ height: 52, borderRadius: radius.md, bgColor: o.disabled ? colors.accentDeep : colors.accent,
|
|
317
|
-
justifyContent: "center", alignItems: "center", name: o.name,
|
|
355
|
+
justifyContent: "center", alignItems: "center", name: o.name, $pressed: { bgColor: colors.accentPressed } })
|
|
318
356
|
|
|
319
357
|
export const SectionLabel = (t: string) =>
|
|
320
358
|
UIText(t.toUpperCase()).style({ fontSize: 11, fontWeight: 600, letterSpacing: 2, color: colors.faint })
|
|
321
359
|
|
|
322
360
|
// A field is a STATIC MOCK — the value is drawn as text (see ui rules); passwords as dots.
|
|
323
361
|
export const Field = (o: { label?: string, value?: string, placeholder?: string, focused?: boolean }) =>
|
|
324
|
-
UIColumn(
|
|
362
|
+
UIColumn(
|
|
325
363
|
o.label ? SectionLabel(o.label) : null,
|
|
326
|
-
UIRow(
|
|
364
|
+
UIRow(UIText(o.value ?? o.placeholder ?? "").style({ flexGrow: 1, fontSize: 16, color: o.value ? colors.text : colors.muted }))
|
|
327
365
|
.style({ height: 52, px: 16, alignItems: "center", bgColor: colors.field, borderRadius: radius.md,
|
|
328
366
|
border: `1px solid ${o.focused ? colors.accent : colors.border}` }),
|
|
329
|
-
|
|
367
|
+
).style({ gap: spacing.sm })
|
|
330
368
|
|
|
331
369
|
// Bottom sheet shell — a real modal: the scrim is overlayColor, the box is just the bottom stack.
|
|
332
|
-
export const Sheet = (children: UINodeChild[]) => {
|
|
370
|
+
export const Sheet = (...children: UINodeChild[]) => {
|
|
333
371
|
const sheet = UIWidget(children).style({
|
|
334
372
|
left: 0, right: 0, bottom: 0, px: spacing.lg, pb: "calc(safe-bottom + 16px)", gap: spacing.sm,
|
|
335
373
|
overlayColor: colors.scrim,
|
|
@@ -339,19 +377,19 @@ export const Sheet = (children: UINodeChild[]) => {
|
|
|
339
377
|
|
|
340
378
|
// Full-screen confirm — full-inset box, centered card, NO onOverlayTap (a confirm demands a choice).
|
|
341
379
|
export const ConfirmDialog = (o: { title: string, body?: string, confirmLabel: string, confirmName: string }) =>
|
|
342
|
-
UIWidget(
|
|
343
|
-
UIColumn(
|
|
380
|
+
UIWidget(
|
|
381
|
+
UIColumn(
|
|
344
382
|
UIText(o.title).style({ fontSize: 17, fontWeight: 700, color: colors.text, textAlign: "center" }),
|
|
345
383
|
o.body ? UIText(o.body).style({ fontSize: 14, color: colors.muted, textAlign: "center", lineHeight: "1.4em" }) : null,
|
|
346
384
|
PrimaryButton("Cancel", { name: "cancel" }),
|
|
347
|
-
|
|
348
|
-
|
|
385
|
+
).style({ bgColor: colors.card, borderRadius: radius.lg, p: spacing.xl, gap: spacing.lg, width: "86%", maxWidth: 360 }),
|
|
386
|
+
).style({ top: 0, left: 0, right: 0, bottom: 0, justifyContent: "center", alignItems: "center", overlayColor: colors.scrim })
|
|
349
387
|
```
|
|
350
388
|
|
|
351
389
|
## Screen rules
|
|
352
390
|
|
|
353
391
|
- **One file = one screen.** `screens/<id>.ts` default-exports either a value
|
|
354
|
-
(`export default UIScreen(
|
|
392
|
+
(`export default UIScreen(...).style({...})`) or a function of state (below). Ids are
|
|
355
393
|
lowercase `[a-z0-9-]`.
|
|
356
394
|
- **Screens are islands.** A screen imports ONLY from `../shared/` — never another screen, no props.
|
|
357
395
|
- **Mock data is the domain.** Realistic inline mock data at the top of the file — the entities and
|
|
@@ -379,20 +417,20 @@ const classes = [
|
|
|
379
417
|
{ start: "19:30", end: "21:00", title: "Acrobatics workshop", trainer: "A. Razgulin", price: 1000, seats: 2 },
|
|
380
418
|
]
|
|
381
419
|
|
|
382
|
-
const FilterSheet = () => Sheet(
|
|
420
|
+
const FilterSheet = () => Sheet(
|
|
383
421
|
/* chip groups… */
|
|
384
422
|
PrimaryButton("Show 1 class", { name: "apply" }),
|
|
385
|
-
|
|
423
|
+
)
|
|
386
424
|
|
|
387
425
|
export default (state: "default" | "empty" | "filters" = "default") => {
|
|
388
426
|
if (state === "filters") FilterSheet().show()
|
|
389
|
-
return UIScreen(
|
|
390
|
-
UIScrollable(
|
|
427
|
+
return UIScreen(
|
|
428
|
+
UIScrollable(
|
|
391
429
|
SectionLabel("Today"),
|
|
392
430
|
state === "empty" ? EmptyDay() : UIColumn(classes.map((c) => ClassCard(c, { name: "class" }))).style({ gap: spacing.sm }),
|
|
393
|
-
|
|
431
|
+
).style({ flexGrow: 1, flexShrink: 1, px: spacing.xl, pt: spacing.md, gap: spacing.lg }),
|
|
394
432
|
mainTabs.bar("schedule"),
|
|
395
|
-
|
|
433
|
+
).style({ bgColor: colors.bg, pt: "safe-top" })
|
|
396
434
|
}
|
|
397
435
|
```
|
|
398
436
|
|
|
@@ -426,60 +464,40 @@ design_map ops: [
|
|
|
426
464
|
## The tab bar
|
|
427
465
|
|
|
428
466
|
Declared ONCE in `shared/tabs.ts` — the board discovers it from source and renders it as a rail
|
|
429
|
-
with lanes; tab switching needs NO edges.
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
bar alone when the screen owns its layout (`mainTabs.bar("schedule")`, as above) or wrap content
|
|
433
|
-
with `mainTabs.screen("home", [...])`.
|
|
434
|
-
|
|
435
|
-
If `shared/tabs.ts` doesn't exist yet and the app needs a tab bar, create it with exactly this
|
|
436
|
-
content (then edit only the `defineTabs({...})` keys and the styling constants):
|
|
467
|
+
with lanes; tab switching needs NO edges. `defineTabs` is an SDK global: the standard themed
|
|
468
|
+
bottom bar, the same look the built app's `UITabs` renders. The keys are screen ids (each tab's
|
|
469
|
+
root, in tab order), literals only; icons are always `assetIcon(...)` calls:
|
|
437
470
|
|
|
438
471
|
```ts
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
const SCREEN_BG = "var(--screenBg, #101114)"
|
|
445
|
-
|
|
446
|
-
export const defineTabs = <T extends Record<string, TabDef>>(tabs: T) => {
|
|
447
|
-
const ids = Object.keys(tabs) as (keyof T & string)[]
|
|
448
|
-
|
|
449
|
-
const bar = (active: keyof T & string) =>
|
|
450
|
-
UIRow(ids.map((id) => {
|
|
451
|
-
const color = id === active ? ACTIVE : INACTIVE
|
|
452
|
-
return UIButton([
|
|
453
|
-
...(tabs[id].icon ? [UIImage(tabs[id].icon!).style({ width: 22, height: 22, tintColor: color })] : []),
|
|
454
|
-
UIText(tabs[id].label).style({ fontSize: 10, color }),
|
|
455
|
-
]).style({ name: `tab-${id}`, flexDirection: "column", gap: 3, flexGrow: 1, flexBase: 0, pt: 8, pb: 6 })
|
|
456
|
-
})).style({ bgColor: BAR_BG, pb: "safe-bottom" })
|
|
457
|
-
|
|
458
|
-
const screen = (active: keyof T & string, children: UINodeChild[]) =>
|
|
459
|
-
UIScreen([
|
|
460
|
-
UIColumn(children).style({ flexGrow: 1, p: 16, pt: "safe-top" }),
|
|
461
|
-
bar(active),
|
|
462
|
-
]).style({ bgColor: SCREEN_BG })
|
|
463
|
-
|
|
464
|
-
return { ids, bar, screen }
|
|
465
|
-
}
|
|
472
|
+
// design/shared/tabs.ts — this declaration is the whole file
|
|
473
|
+
export const mainTabs = defineTabs({
|
|
474
|
+
schedule: { label: "Schedule", icon: assetIcon("lucide:calendar") },
|
|
475
|
+
profile: { label: "Profile", icon: assetIcon("lucide:user") },
|
|
476
|
+
})
|
|
466
477
|
```
|
|
467
478
|
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
`
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
479
|
+
A screen that shows the bar mounts it with a literal tab id — roots AND pushed details that keep
|
|
480
|
+
the bar; a screen that doesn't mount it is a full-screen push. Mount the bar alone when the screen
|
|
481
|
+
owns its layout (`mainTabs.bar("schedule")`, as above) or wrap content with
|
|
482
|
+
`mainTabs.screen("home", [...])`. Restyle through the theme's system keys — `primaryColor`,
|
|
483
|
+
`mutedColor`, `tabbarBg`, `screenBg`, set in `shared/tokens.ts` — never by reimplementing the
|
|
484
|
+
bar. Keep the `defineTabs({...})` declaration and `.screen("<tab>", ...)` / `.bar("<tab>")` call
|
|
485
|
+
shapes intact — the board parses them.
|
|
486
|
+
|
|
487
|
+
## Icons & fonts
|
|
488
|
+
|
|
489
|
+
`assetIcon("pack:name")` inlines a registry icon at compile time — the ONLY way to make an icon
|
|
490
|
+
(never hand-write its `{ svg }` result — that renders blank). Use real icons freely on every
|
|
491
|
+
screen. Vendored copies (`design/assets/icons/`, listed under `[Assets]`) resolve first; anything
|
|
492
|
+
else is fetched from the icon registry automatically. The main pack is `lucide` (kebab-case names:
|
|
493
|
+
`"lucide:bell"`, `"lucide:chevron-right"`); an unknown name is a compile error with "did you mean"
|
|
494
|
+
suggestions. Wrap each icon in a tiny factory in `shared/ui.ts`
|
|
495
|
+
(`Icon.bell = (c, s = 20) => UIImage(assetIcon("lucide:bell")).style({ width: s, height: s, tintColor: c })`).
|
|
496
|
+
|
|
497
|
+
`font("id")` works the same way — registry families load from the font CDN, no vendoring needed:
|
|
498
|
+
`theme({ fontFamily: font("manrope") })`, or per-node `.style({ fontFamily: font("rubik") })`.
|
|
499
|
+
An unknown id or weight is a compile error listing what exists. Only project font files
|
|
500
|
+
(`font("./Brand.ttf")`) must already be pushed.
|
|
483
501
|
|
|
484
502
|
## spec.md — the concept
|
|
485
503
|
|