lecodes-sdk 2.0.4 → 2.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +104 -76
- package/dist/global.d.ts +2 -5
- package/dist/host.d.ts +3 -0
- package/dist/types/inject.d.ts +3 -3
- package/dist/types/runtime/device.d.ts +7 -0
- package/dist/types/runtime/rpc.d.ts +11 -17
- package/dist/types/runtime/wire.d.ts +53 -0
- package/dist/types/server/auth/api.d.ts +42 -0
- package/dist/types/server/auth/appConfig.d.ts +1 -5
- package/dist/types/server/auth/models.d.ts +119 -70
- package/dist/types/server/auth/types.d.ts +19 -43
- package/dist/types/server/channel.d.ts +57 -19
- package/dist/types/server/context.d.ts +2 -2
- package/dist/types/server/db/defineDb.d.ts +10 -0
- package/dist/types/server/db/index.d.ts +1 -1
- package/dist/types/server/db/types.d.ts +76 -6
- package/dist/types/server/inject.d.ts +0 -1
- package/dist/types/ui/UINode.d.ts +19 -5
- package/dist/types/ui/UIScreen.d.ts +1 -0
- package/dist/types/ui/UITabs.d.ts +8 -6
- package/dist/types/ui/theme.d.ts +48 -13
- package/dist/types/version.d.ts +1 -1
- package/dist/types.json +1 -1
- package/package.json +3 -2
- package/prompts/README.md +1 -1
- package/prompts/design.md +19 -19
- package/prompts/dist/2d-game.md +45 -31
- package/prompts/dist/3d-app.md +45 -31
- package/prompts/dist/ar-app.md +45 -31
- package/prompts/dist/design.md +25 -24
- package/prompts/dist/ui-app.md +45 -31
- package/prompts/ui-design.md +6 -5
- package/prompts/ui.md +25 -22
- package/src/bridges/device.d.ts +9 -0
- package/src/bridges/tree.d.ts +5 -0
- package/src/chisel.ts +1 -1
- package/src/compile/bundler.ts +6 -0
- package/src/compile/compileProject.ts +3 -1
- package/src/compile/index.ts +3 -1
- package/src/compile/serverSplit.ts +58 -11
- package/src/compile/serverTypes.ts +189 -8
- package/src/host.d.ts +3 -0
- package/src/inject.ts +6 -6
- package/src/runtime/device.ts +12 -0
- package/src/runtime/rpc.ts +101 -40
- package/src/runtime/wire.ts +35 -0
- package/src/server/auth/api.ts +94 -0
- package/src/server/auth/appConfig.ts +2 -3
- package/src/server/auth/host.ts +244 -174
- package/src/server/auth/models.ts +45 -62
- package/src/server/auth/types.ts +19 -34
- package/src/server/channel.ts +97 -29
- package/src/server/channelHub.ts +153 -0
- package/src/server/context.ts +2 -2
- package/src/server/db/defineDb.ts +96 -36
- package/src/server/db/index.ts +1 -1
- package/src/server/db/types.ts +76 -8
- package/src/server/host.ts +25 -10
- package/src/server/inject.ts +2 -2
- package/src/server/runtime.ts +34 -12
- package/src/ui/UINode.ts +22 -5
- package/src/ui/UIScreen.ts +5 -0
- package/src/ui/UITabs.ts +19 -17
- package/src/ui/styleColor.ts +10 -1
- package/src/ui/theme.ts +96 -41
- package/src/version.ts +1 -1
- package/tests/helpers/fakeTree.ts +1 -0
- package/dist/types/plugins/oauth.d.ts +0 -25
- package/dist/types/server/auth/global.d.ts +0 -56
- package/src/plugins/oauth.ts +0 -61
- package/src/server/auth/global.ts +0 -80
- package/tests/helpers/memoryMarci.ts +0 -124
package/package.json
CHANGED
|
@@ -41,9 +41,10 @@
|
|
|
41
41
|
"@types/bun": "^1.3.14",
|
|
42
42
|
"@types/node": "^25.9.1",
|
|
43
43
|
"typescript": "~5.8.3",
|
|
44
|
-
"gl-matrix": "^3.4.4"
|
|
44
|
+
"gl-matrix": "^3.4.4",
|
|
45
|
+
"marcidb-embedded": "^0.13.0"
|
|
45
46
|
},
|
|
46
|
-
"version": "2.0.
|
|
47
|
+
"version": "2.0.5",
|
|
47
48
|
"files": [
|
|
48
49
|
"src",
|
|
49
50
|
"dist",
|
package/prompts/README.md
CHANGED
|
@@ -136,7 +136,7 @@ escalation directive (above) lets it swap in the right engine module instead of
|
|
|
136
136
|
- `index.md` must list every global exported from `sdk/src/inject.ts` — same contract as
|
|
137
137
|
`docs/check.mjs`. When a global is added, update docs, then the module, then index.md.
|
|
138
138
|
- Known corrections baked into these prompts (do not regress; see `docs/AUTHORING.md`):
|
|
139
|
-
`onEndReached(thresholdPx, cb)` (threshold first), `bgSize: cover|contain|tile
|
|
139
|
+
`onEndReached(thresholdPx, cb)` (threshold first), `bgSize: cover|contain|fill|tile`,
|
|
140
140
|
track `deltaX/deltaY` are per-move, colors are CSS strings / 0xRRGGBB / [r,g,b(,a)] everywhere, `animate` can't
|
|
141
141
|
tween strings/colors, `node.physics` (not `.body`), no `Camera2D.follow`, no
|
|
142
142
|
`UIScrollable.scrollTo`, models can't be cloned.
|
package/prompts/design.md
CHANGED
|
@@ -25,20 +25,20 @@ composition:
|
|
|
25
25
|
|
|
26
26
|
```ts
|
|
27
27
|
// design/shared/tokens.ts — surfaces dark-to-light, text in tiers, accents as a family.
|
|
28
|
-
// ONE theme() call registers the vars and returns the accessors ("var(--x)" strings)
|
|
29
|
-
//
|
|
30
|
-
//
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
28
|
+
// ONE theme() call registers the vars and returns the accessors ("var(--x)" strings). Re-calling
|
|
29
|
+
// theme() with new values (dark mode, brand swap) restyles every screen — and later the built app
|
|
30
|
+
// — live.
|
|
31
|
+
export const colors = theme({
|
|
32
|
+
// the ROLES — the SDK reads these too: bg = every screen's background, text = text that names
|
|
33
|
+
// no color, surface / border / textMuted / accent = the tab bar (defineTabs here, UITabs in the
|
|
34
|
+
// built app). Name the palette by them; there are no separate keys for the chrome.
|
|
35
|
+
bg: "#080808", surface: "#161618", border: "#2a2a2e",
|
|
36
|
+
text: "#ffffff", textMuted: "#8a8a93", accent: "#00BFC2",
|
|
37
|
+
// the design's own
|
|
38
|
+
surfaceAlt: "#1d1d20", field: "#101012", faint: "#5a5b62",
|
|
39
|
+
accentPressed: "#00999b", accentDeep: "#0c4e4f", onAccent: "#05201f",
|
|
35
40
|
success: "#30D158", danger: "#F83131", scrim: "#000000b3",
|
|
36
|
-
|
|
37
|
-
// the other four brand the tab bar (defineTabs here, UITabs in the built app). Mirror the brand:
|
|
38
|
-
color: "#ffffff",
|
|
39
|
-
primaryColor: "#00BFC2", mutedColor: "#8a8a93", tabbarBg: "#161618", screenBg: "#080808",
|
|
40
|
-
}
|
|
41
|
-
export const colors: { [K in keyof typeof palette]: string } = theme(palette)
|
|
41
|
+
})
|
|
42
42
|
export const spacing = { xs: 4, sm: 8, md: 12, lg: 16, xl: 20, xxl: 24 }
|
|
43
43
|
export const radius = { sm: 8, md: 12, lg: 16, pill: 999 }
|
|
44
44
|
```
|
|
@@ -48,7 +48,7 @@ export const radius = { sm: 8, md: 12, lg: 16, pill: 999 }
|
|
|
48
48
|
import { colors, spacing, radius } from "./tokens"
|
|
49
49
|
|
|
50
50
|
export const PrimaryButton = (label: string, o: { name?: string, disabled?: boolean } = {}) =>
|
|
51
|
-
UIButton(UIText(label).style({ fontSize: 16, fontWeight: 700, color: o.disabled ? colors.
|
|
51
|
+
UIButton(UIText(label).style({ fontSize: 16, fontWeight: 700, color: o.disabled ? colors.textMuted : colors.onAccent }))
|
|
52
52
|
.style({ height: 52, borderRadius: radius.md, bgColor: o.disabled ? colors.accentDeep : colors.accent,
|
|
53
53
|
justifyContent: "center", alignItems: "center", name: o.name, $pressed: { bgColor: colors.accentPressed } })
|
|
54
54
|
|
|
@@ -59,7 +59,7 @@ export const SectionLabel = (t: string) =>
|
|
|
59
59
|
export const Field = (o: { label?: string, value?: string, placeholder?: string, focused?: boolean }) =>
|
|
60
60
|
UIColumn(
|
|
61
61
|
o.label ? SectionLabel(o.label) : null,
|
|
62
|
-
UIRow(UIText(o.value ?? o.placeholder ?? "").style({ flexGrow: 1, fontSize: 16, color: o.value ? colors.text : colors.
|
|
62
|
+
UIRow(UIText(o.value ?? o.placeholder ?? "").style({ flexGrow: 1, fontSize: 16, color: o.value ? colors.text : colors.textMuted }))
|
|
63
63
|
.style({ height: 52, px: 16, alignItems: "center", bgColor: colors.field, borderRadius: radius.md,
|
|
64
64
|
border: `1px solid ${o.focused ? colors.accent : colors.border}` }),
|
|
65
65
|
).style({ gap: spacing.sm })
|
|
@@ -78,9 +78,9 @@ export const ConfirmDialog = (o: { title: string, body?: string, confirmLabel: s
|
|
|
78
78
|
UIWidget(
|
|
79
79
|
UIColumn(
|
|
80
80
|
UIText(o.title).style({ fontSize: 17, fontWeight: 700, color: colors.text, textAlign: "center" }),
|
|
81
|
-
o.body ? UIText(o.body).style({ fontSize: 14, color: colors.
|
|
81
|
+
o.body ? UIText(o.body).style({ fontSize: 14, color: colors.textMuted, textAlign: "center", lineHeight: "1.4em" }) : null,
|
|
82
82
|
PrimaryButton("Cancel", { name: "cancel" }),
|
|
83
|
-
).style({ bgColor: colors.
|
|
83
|
+
).style({ bgColor: colors.surface, borderRadius: radius.lg, p: spacing.xl, gap: spacing.lg, width: "86%", maxWidth: 360 }),
|
|
84
84
|
).style({ top: 0, left: 0, right: 0, bottom: 0, justifyContent: "center", alignItems: "center", overlayColor: colors.scrim })
|
|
85
85
|
```
|
|
86
86
|
|
|
@@ -177,8 +177,8 @@ export const mainTabs = defineTabs({
|
|
|
177
177
|
A screen that shows the bar mounts it with a literal tab id — roots AND pushed details that keep
|
|
178
178
|
the bar; a screen that doesn't mount it is a full-screen push. Mount the bar alone when the screen
|
|
179
179
|
owns its layout (`mainTabs.bar("schedule")`, as above) or wrap content with
|
|
180
|
-
`mainTabs.screen("home", [...])`.
|
|
181
|
-
`
|
|
180
|
+
`mainTabs.screen("home", [...])`. It is colored by the theme's roles — `accent`,
|
|
181
|
+
`textMuted`, `surface`, `border`, `bg`, set in `shared/tokens.ts` — never by reimplementing the
|
|
182
182
|
bar. Keep the `defineTabs({...})` declaration and `.screen("<tab>", ...)` / `.bar("<tab>")` call
|
|
183
183
|
shapes intact — the board parses them.
|
|
184
184
|
|
package/prompts/dist/2d-game.md
CHANGED
|
@@ -795,8 +795,8 @@ Router.init(tabs) // UITabs IS a UIScreen — present it directly
|
|
|
795
795
|
// .select(id), .tab (getter), .onSelect(cb(id, i)) — fires on a bar tap, swipe, or select()
|
|
796
796
|
// .badge(id, value) — true = dot, number/string = count pill, false/null/0 clears
|
|
797
797
|
// .pager — the UIPager underneath; UIPager.push(detail) from any screen keeps the bar
|
|
798
|
-
//
|
|
799
|
-
//
|
|
798
|
+
// The bar is colored by the theme's roles (accent, textMuted, surface, border) — no keys of its
|
|
799
|
+
// own; one bar's own colors: tabs.bar.theme({ accent, surface }). Custom layout: UIPager below.
|
|
800
800
|
|
|
801
801
|
// UIPager — the navigation primitive under UITabs: sibling tabs that swipe natively, each tab its
|
|
802
802
|
// OWN push/pop stack. Reach for it directly for a plain stack (one-screen pager) or a fully custom
|
|
@@ -1012,7 +1012,7 @@ const btn: Style<UIButton> = { bgColor: "#333", borderRadius: 12 }
|
|
|
1012
1012
|
// Padding: p, px, py, pt, pb, pl, pr (≡ padding, paddingHorizontal, paddingVertical, paddingTop, ...)
|
|
1013
1013
|
// Margin: m, mx, my, mt, mb, ml, mr — margins also accept "auto" (mx: "auto" centers a fixed-width element)
|
|
1014
1014
|
// Background (containers/screen/button/input/video; UIText/UIImage/UISpacer have bgColor only):
|
|
1015
|
-
// bgColor, bgImage (string | FetchResponse | File), bgSize ("cover"|"contain"|"tile"), bgGradient
|
|
1015
|
+
// bgColor, bgImage (string | FetchResponse | File | SvgSource), bgSize ("cover"|"contain"|"fill"|"tile"), bgGradient
|
|
1016
1016
|
// bgGradient: a CSS linear-gradient() or radial-gradient() string, e.g. "linear-gradient(to top, rgba(0,0,0,0.7), transparent)"
|
|
1017
1017
|
// or "radial-gradient(circle at 50% 40%, #7B2FF7, #0a0a1a)". Comma-separate several gradients to stack them (first = on top).
|
|
1018
1018
|
// Layers paint bottom-to-top: bgColor → bgImage → bgGradient (gradient over image = text scrim).
|
|
@@ -1039,23 +1039,27 @@ const btn: Style<UIButton> = { bgColor: "#333", borderRadius: 12 }
|
|
|
1039
1039
|
// Env keywords resolving to the device insets (notch, home indicator):
|
|
1040
1040
|
// "safe-top" | "safe-bottom" | "safe-left" | "safe-right"; "safe-all" — p / m shorthands only
|
|
1041
1041
|
// Comfort tokens — where content comfortably starts, never collapse to 0: max(safe-edge, knob) on clearance
|
|
1042
|
-
// insets (iOS notch/home indicator
|
|
1043
|
-
// knob ADDS instead. Knob defaults: 12 top,
|
|
1042
|
+
// insets (iOS notch/home indicator); past exact-height system BARS (Android status/nav bar) the
|
|
1043
|
+
// knob ADDS instead. Knob defaults: 12 top, 8 bottom, 16 horizontal (the page gutter):
|
|
1044
1044
|
// "comfort-top" | "comfort-bottom" | "comfort-left" | "comfort-right"
|
|
1045
1045
|
// "comfort-x" — px/mx | "comfort-y" — py/my | "comfort-all" — p / m
|
|
1046
1046
|
// Accepted on per-side padding & margin, position offsets (top/left/bottom/right), and inside calc()/min()/max():
|
|
1047
|
-
.style({ pb: "comfort-bottom" }) // bottom bar: 34 over a home indicator,
|
|
1047
|
+
.style({ pb: "comfort-bottom" }) // bottom bar: 34 over a home indicator, 56 above Android's button bar, 8 on inset-less devices
|
|
1048
1048
|
.style({ px: "comfort-x" }) // page gutter that also clears the notch in landscape
|
|
1049
1049
|
.style({ pb: "calc(comfort-bottom + 56px)" }) // scroll content clearing a 56px floating bar
|
|
1050
1050
|
|
|
1051
1051
|
// --- Theme variables ---
|
|
1052
1052
|
// theme({...}) merges app vars into one table; styles read them with "var(--name)" (numbers are px
|
|
1053
1053
|
// lengths, null removes a key). Re-calling re-styles the LIVE UI — dark mode is a second call.
|
|
1054
|
-
// Returned accessors ARE the var() strings
|
|
1055
|
-
const T = theme({
|
|
1056
|
-
label.style({ color: T.
|
|
1057
|
-
//
|
|
1058
|
-
//
|
|
1054
|
+
// ONE call per app. Returned accessors ARE the var() strings:
|
|
1055
|
+
const T = theme({ accent: "#0A84FF", "comfort-left": 20 })
|
|
1056
|
+
label.style({ color: T.accent, pl: "var(--comfort-left)" })
|
|
1057
|
+
// ROLES — keys the SDK reads itself; use them as your palette and add your own keys beside them:
|
|
1058
|
+
// bg (every screen's background) · surface (cards, bars; the tab bar) · border (hairlines)
|
|
1059
|
+
// text (text with no color of its own) · textMuted (secondary; inactive tabs) · accent (active tab)
|
|
1060
|
+
// fontFamily (text with no font of its own). Light theme = set text once, not a color per label.
|
|
1061
|
+
// SCOPE: el.theme({...}) — the same keys for ONE subtree (a card, tabs.bar, a whole screen); its
|
|
1062
|
+
// descendants read them first. A pushed screen / a widget is its own root: give it its own.
|
|
1059
1063
|
// Bare comfort-* tokens apply the safe-area formula; "var(--comfort-top)" reads the raw knob.
|
|
1060
1064
|
// "var(--name, fallback)" applies the fallback while the key is unset. Env names (safe-*, vw/vh) are not theme keys.
|
|
1061
1065
|
|
|
@@ -1087,7 +1091,7 @@ screen.style({ flexDirection: "column", p: 16, $landscape: { flexDirection: "row
|
|
|
1087
1091
|
// alignItems: "stretch" — children FILL the cross axis by default; set a size or alignSelf to opt out.
|
|
1088
1092
|
// overflow: "hidden" — children are clipped to the parent's box; set overflow: "visible" to let them escape.
|
|
1089
1093
|
// boxSizing: "border-box" — width/height include padding & border.
|
|
1090
|
-
// Screen bg is black, default text is WHITE at fontSize 14 (retarget once via theme({
|
|
1094
|
+
// Screen bg is black, default text is WHITE at fontSize 14 (retarget once via theme({ bg, text, fontFamily })).
|
|
1091
1095
|
// On any light surface — white cards, sheets, inputs — white text renders invisible: use dark text there
|
|
1092
1096
|
// ("#111"/black), checking every UIText against the surface it actually sits on, not the screen.
|
|
1093
1097
|
|
|
@@ -1161,14 +1165,25 @@ UIButton().onLayout(({ width }) => { buttonWidth = width })
|
|
|
1161
1165
|
// ===== ROUTER (multi-page apps) =====
|
|
1162
1166
|
// Tabs are UITabs' job, in-tab stacks UIPager's (see UI COMPONENTS) — the Router owns what sits ABOVE the shell.
|
|
1163
1167
|
Router.init(homeScreen, opts?: { showDefaultBackButton?: boolean }) // call once in the entry file (default false)
|
|
1164
|
-
Router.push(screen)
|
|
1165
|
-
Router.pop(to?: number
|
|
1168
|
+
Router.push(screen, opts?: { transition, popTransition }) // push onto the stack, screen becomes active (default "push")
|
|
1169
|
+
Router.pop(to?: number, opts?: { transition })
|
|
1170
|
+
// default -1 = one back; negative = relative (-2 = back two), 0/positive = absolute
|
|
1166
1171
|
// stack index (0 = home). Popped screens' native trees are destroyed; the UIScreen
|
|
1167
|
-
// object stays reusable (push remounts it).
|
|
1168
|
-
Router.replace(screen, opts?: { transition }) // swap the top screen
|
|
1169
|
-
// "slide-from-right" | "slide-from-top" | "slide-from-bottom" | "zoom" | "zoom-out" | "zoom-in" | "fade" | "none" (default "fade")
|
|
1172
|
+
// object stays reusable (push remounts it). Plays the way back the screen remembers.
|
|
1173
|
+
Router.replace(screen, opts?: { transition, popTransition }) // swap the top screen (default "none"; theme({ replaceTransition }))
|
|
1170
1174
|
Router.current // the active UIScreen (getter)
|
|
1171
|
-
Router.hide() / Router.restore() // hide/restore the whole router — stack stays alive; fires the top screen's onClose/onOpen
|
|
1175
|
+
Router.hide(opts?) / Router.restore(opts?) // hide/restore the whole router — stack stays alive; fires the top screen's onClose/onOpen
|
|
1176
|
+
// TRANSITIONS — every call above, and open() / close() of any destination, takes { transition }:
|
|
1177
|
+
// a name: "push" | "pop" | "slide-from-left" | "slide-from-right" | "slide-from-top" | "slide-from-bottom" |
|
|
1178
|
+
// "zoom" | "zoom-in" | "zoom-out" | "fade" | "none"
|
|
1179
|
+
// or your own: { enter?: pose, exit?: pose, onTop?: "enter" | "exit" } — the incoming screen comes FROM `enter`,
|
|
1180
|
+
// the outgoing one goes TO `exit`. pose = { transform?, opacity?, dim?, duration? (ms, 300), delay?, easing? };
|
|
1181
|
+
// arrays are keyframes; in a pose a % in translate is a percent of the screen ("translateY(100%)").
|
|
1182
|
+
// e.g. Router.push(page, { transition: { enter: { transform: "translateY(100%)", duration: 400 }, exit: { dim: 0.3 } } })
|
|
1183
|
+
// popTransition = the way BACK the screen remembers (Router.pop(), the back gesture, the system's back play it).
|
|
1184
|
+
// Default: the transition it came with, the other way round (a slide from the bottom leaves downwards;
|
|
1185
|
+
// a zoom / a fade go back with a fade). A replace without one keeps the way back of the screen it replaces.
|
|
1186
|
+
// A screen's own transform / opacity are not seen while a transition moves it — style a child instead.
|
|
1172
1187
|
Router.addEventListener("change", (screen: UIScreen) => ...) / .removeEventListener("change", cb) // after every navigation
|
|
1173
1188
|
// Screens BELOW the top stay mounted — element trees and state survive, they're just not rendered.
|
|
1174
1189
|
// push fires the outgoing screen's onClose + the incoming one's onOpen; pop fires them in reverse and
|
|
@@ -1190,7 +1205,7 @@ font("ibm-plex-sans", { weights: [400, 600], italic: true }) // narrow/extend t
|
|
|
1190
1205
|
// A project's own file: font("./fonts/Brand.ttf") → real family name read from the file itself
|
|
1191
1206
|
// (weight/italic too — no options); two weight files of one family return the same name.
|
|
1192
1207
|
// registerFont(family, url, {weight, style}) survives ONLY for runtime-computed URLs — rare.
|
|
1193
|
-
//
|
|
1208
|
+
// The app's font: theme({ fontFamily }). A second family is set per UIText (extract a shared Style<UIText>).
|
|
1194
1209
|
|
|
1195
1210
|
// ===== SVG IMAGES =====
|
|
1196
1211
|
// SvgSource wraps raw SVG XML for use as an image source: UIImage(SvgSource(`<svg ...>`)).
|
|
@@ -1292,17 +1307,16 @@ screen.open()
|
|
|
1292
1307
|
</file>
|
|
1293
1308
|
|
|
1294
1309
|
// === EXAMPLE 2: Theme tokens + fetched list with loading / error states ===
|
|
1295
|
-
// A tokens module every screen imports, light-themed
|
|
1296
|
-
//
|
|
1310
|
+
// A tokens module every screen imports, light-themed. ONE theme() call: the roles (bg, surface,
|
|
1311
|
+
// border, text, textMuted, accent) color the screens, the default text and the tab bar — no
|
|
1312
|
+
// color: "#111" on every label — and the app's own keys sit beside them.
|
|
1297
1313
|
<file name="tokens.ts">
|
|
1298
|
-
const palette = {
|
|
1299
|
-
bg: "#F4F6F5", card: "#FFFFFF", border: "#E4E8E6",
|
|
1300
|
-
text: "#131A17", muted: "#606B65",
|
|
1301
|
-
accent: "#15A34A", accentSoft: "#E7F6ED", onAccent: "#FFFFFF",
|
|
1302
|
-
}
|
|
1303
|
-
theme({ color: palette.text, primaryColor: palette.accent })
|
|
1304
1314
|
// Accessors ARE "var(--x)" strings — re-calling theme() with new values restyles the live app.
|
|
1305
|
-
export const colors
|
|
1315
|
+
export const colors = theme({
|
|
1316
|
+
bg: "#F4F6F5", surface: "#FFFFFF", border: "#E4E8E6",
|
|
1317
|
+
text: "#131A17", textMuted: "#606B65", accent: "#15A34A",
|
|
1318
|
+
accentSoft: "#E7F6ED", onAccent: "#FFFFFF",
|
|
1319
|
+
})
|
|
1306
1320
|
export const font = { // type scale — spread into styles: .style({ ...font.h2 })
|
|
1307
1321
|
h2: { fontSize: 22, fontWeight: 700 }, bodyStrong: { fontSize: 16, fontWeight: 600 },
|
|
1308
1322
|
small: { fontSize: 14 }, tiny: { fontSize: 12, fontWeight: 500 },
|
|
@@ -1319,9 +1333,9 @@ const Row = (u: User) => UIRow(
|
|
|
1319
1333
|
justifyContent: "center", alignItems: "center" }),
|
|
1320
1334
|
UIColumn(
|
|
1321
1335
|
UIText(u.name).style({ ...font.bodyStrong }),
|
|
1322
|
-
UIText(u.email).style({ ...font.small, color: colors.
|
|
1336
|
+
UIText(u.email).style({ ...font.small, color: colors.textMuted }),
|
|
1323
1337
|
).style({ flexGrow: 1, flexShrink: 1, gap: 2, alignItems: "flex-start" }),
|
|
1324
|
-
).style({ alignItems: "center", gap: 12, bgColor: colors.
|
|
1338
|
+
).style({ alignItems: "center", gap: 12, bgColor: colors.surface, borderRadius: 16,
|
|
1325
1339
|
border: `1px solid ${colors.border}`, p: 14 })
|
|
1326
1340
|
|
|
1327
1341
|
const Centered = (...children: UINodeChild[]) =>
|
|
@@ -1330,7 +1344,7 @@ const Centered = (...children: UINodeChild[]) =>
|
|
|
1330
1344
|
let body: UIColumn
|
|
1331
1345
|
|
|
1332
1346
|
const loadData = async () => {
|
|
1333
|
-
body.setContent([Centered(UIText("Loading…").style({ color: colors.
|
|
1347
|
+
body.setContent([Centered(UIText("Loading…").style({ color: colors.textMuted }))])
|
|
1334
1348
|
const res = await fetch("https://jsonplaceholder.typicode.com/users")
|
|
1335
1349
|
if (res.status !== 200) {
|
|
1336
1350
|
body.setContent([Centered(
|
package/prompts/dist/3d-app.md
CHANGED
|
@@ -937,8 +937,8 @@ Router.init(tabs) // UITabs IS a UIScreen — present it directly
|
|
|
937
937
|
// .select(id), .tab (getter), .onSelect(cb(id, i)) — fires on a bar tap, swipe, or select()
|
|
938
938
|
// .badge(id, value) — true = dot, number/string = count pill, false/null/0 clears
|
|
939
939
|
// .pager — the UIPager underneath; UIPager.push(detail) from any screen keeps the bar
|
|
940
|
-
//
|
|
941
|
-
//
|
|
940
|
+
// The bar is colored by the theme's roles (accent, textMuted, surface, border) — no keys of its
|
|
941
|
+
// own; one bar's own colors: tabs.bar.theme({ accent, surface }). Custom layout: UIPager below.
|
|
942
942
|
|
|
943
943
|
// UIPager — the navigation primitive under UITabs: sibling tabs that swipe natively, each tab its
|
|
944
944
|
// OWN push/pop stack. Reach for it directly for a plain stack (one-screen pager) or a fully custom
|
|
@@ -1154,7 +1154,7 @@ const btn: Style<UIButton> = { bgColor: "#333", borderRadius: 12 }
|
|
|
1154
1154
|
// Padding: p, px, py, pt, pb, pl, pr (≡ padding, paddingHorizontal, paddingVertical, paddingTop, ...)
|
|
1155
1155
|
// Margin: m, mx, my, mt, mb, ml, mr — margins also accept "auto" (mx: "auto" centers a fixed-width element)
|
|
1156
1156
|
// Background (containers/screen/button/input/video; UIText/UIImage/UISpacer have bgColor only):
|
|
1157
|
-
// bgColor, bgImage (string | FetchResponse | File), bgSize ("cover"|"contain"|"tile"), bgGradient
|
|
1157
|
+
// bgColor, bgImage (string | FetchResponse | File | SvgSource), bgSize ("cover"|"contain"|"fill"|"tile"), bgGradient
|
|
1158
1158
|
// bgGradient: a CSS linear-gradient() or radial-gradient() string, e.g. "linear-gradient(to top, rgba(0,0,0,0.7), transparent)"
|
|
1159
1159
|
// or "radial-gradient(circle at 50% 40%, #7B2FF7, #0a0a1a)". Comma-separate several gradients to stack them (first = on top).
|
|
1160
1160
|
// Layers paint bottom-to-top: bgColor → bgImage → bgGradient (gradient over image = text scrim).
|
|
@@ -1181,23 +1181,27 @@ const btn: Style<UIButton> = { bgColor: "#333", borderRadius: 12 }
|
|
|
1181
1181
|
// Env keywords resolving to the device insets (notch, home indicator):
|
|
1182
1182
|
// "safe-top" | "safe-bottom" | "safe-left" | "safe-right"; "safe-all" — p / m shorthands only
|
|
1183
1183
|
// Comfort tokens — where content comfortably starts, never collapse to 0: max(safe-edge, knob) on clearance
|
|
1184
|
-
// insets (iOS notch/home indicator
|
|
1185
|
-
// knob ADDS instead. Knob defaults: 12 top,
|
|
1184
|
+
// insets (iOS notch/home indicator); past exact-height system BARS (Android status/nav bar) the
|
|
1185
|
+
// knob ADDS instead. Knob defaults: 12 top, 8 bottom, 16 horizontal (the page gutter):
|
|
1186
1186
|
// "comfort-top" | "comfort-bottom" | "comfort-left" | "comfort-right"
|
|
1187
1187
|
// "comfort-x" — px/mx | "comfort-y" — py/my | "comfort-all" — p / m
|
|
1188
1188
|
// Accepted on per-side padding & margin, position offsets (top/left/bottom/right), and inside calc()/min()/max():
|
|
1189
|
-
.style({ pb: "comfort-bottom" }) // bottom bar: 34 over a home indicator,
|
|
1189
|
+
.style({ pb: "comfort-bottom" }) // bottom bar: 34 over a home indicator, 56 above Android's button bar, 8 on inset-less devices
|
|
1190
1190
|
.style({ px: "comfort-x" }) // page gutter that also clears the notch in landscape
|
|
1191
1191
|
.style({ pb: "calc(comfort-bottom + 56px)" }) // scroll content clearing a 56px floating bar
|
|
1192
1192
|
|
|
1193
1193
|
// --- Theme variables ---
|
|
1194
1194
|
// theme({...}) merges app vars into one table; styles read them with "var(--name)" (numbers are px
|
|
1195
1195
|
// lengths, null removes a key). Re-calling re-styles the LIVE UI — dark mode is a second call.
|
|
1196
|
-
// Returned accessors ARE the var() strings
|
|
1197
|
-
const T = theme({
|
|
1198
|
-
label.style({ color: T.
|
|
1199
|
-
//
|
|
1200
|
-
//
|
|
1196
|
+
// ONE call per app. Returned accessors ARE the var() strings:
|
|
1197
|
+
const T = theme({ accent: "#0A84FF", "comfort-left": 20 })
|
|
1198
|
+
label.style({ color: T.accent, pl: "var(--comfort-left)" })
|
|
1199
|
+
// ROLES — keys the SDK reads itself; use them as your palette and add your own keys beside them:
|
|
1200
|
+
// bg (every screen's background) · surface (cards, bars; the tab bar) · border (hairlines)
|
|
1201
|
+
// text (text with no color of its own) · textMuted (secondary; inactive tabs) · accent (active tab)
|
|
1202
|
+
// fontFamily (text with no font of its own). Light theme = set text once, not a color per label.
|
|
1203
|
+
// SCOPE: el.theme({...}) — the same keys for ONE subtree (a card, tabs.bar, a whole screen); its
|
|
1204
|
+
// descendants read them first. A pushed screen / a widget is its own root: give it its own.
|
|
1201
1205
|
// Bare comfort-* tokens apply the safe-area formula; "var(--comfort-top)" reads the raw knob.
|
|
1202
1206
|
// "var(--name, fallback)" applies the fallback while the key is unset. Env names (safe-*, vw/vh) are not theme keys.
|
|
1203
1207
|
|
|
@@ -1229,7 +1233,7 @@ screen.style({ flexDirection: "column", p: 16, $landscape: { flexDirection: "row
|
|
|
1229
1233
|
// alignItems: "stretch" — children FILL the cross axis by default; set a size or alignSelf to opt out.
|
|
1230
1234
|
// overflow: "hidden" — children are clipped to the parent's box; set overflow: "visible" to let them escape.
|
|
1231
1235
|
// boxSizing: "border-box" — width/height include padding & border.
|
|
1232
|
-
// Screen bg is black, default text is WHITE at fontSize 14 (retarget once via theme({
|
|
1236
|
+
// Screen bg is black, default text is WHITE at fontSize 14 (retarget once via theme({ bg, text, fontFamily })).
|
|
1233
1237
|
// On any light surface — white cards, sheets, inputs — white text renders invisible: use dark text there
|
|
1234
1238
|
// ("#111"/black), checking every UIText against the surface it actually sits on, not the screen.
|
|
1235
1239
|
|
|
@@ -1303,14 +1307,25 @@ UIButton().onLayout(({ width }) => { buttonWidth = width })
|
|
|
1303
1307
|
// ===== ROUTER (multi-page apps) =====
|
|
1304
1308
|
// Tabs are UITabs' job, in-tab stacks UIPager's (see UI COMPONENTS) — the Router owns what sits ABOVE the shell.
|
|
1305
1309
|
Router.init(homeScreen, opts?: { showDefaultBackButton?: boolean }) // call once in the entry file (default false)
|
|
1306
|
-
Router.push(screen)
|
|
1307
|
-
Router.pop(to?: number
|
|
1310
|
+
Router.push(screen, opts?: { transition, popTransition }) // push onto the stack, screen becomes active (default "push")
|
|
1311
|
+
Router.pop(to?: number, opts?: { transition })
|
|
1312
|
+
// default -1 = one back; negative = relative (-2 = back two), 0/positive = absolute
|
|
1308
1313
|
// stack index (0 = home). Popped screens' native trees are destroyed; the UIScreen
|
|
1309
|
-
// object stays reusable (push remounts it).
|
|
1310
|
-
Router.replace(screen, opts?: { transition }) // swap the top screen
|
|
1311
|
-
// "slide-from-right" | "slide-from-top" | "slide-from-bottom" | "zoom" | "zoom-out" | "zoom-in" | "fade" | "none" (default "fade")
|
|
1314
|
+
// object stays reusable (push remounts it). Plays the way back the screen remembers.
|
|
1315
|
+
Router.replace(screen, opts?: { transition, popTransition }) // swap the top screen (default "none"; theme({ replaceTransition }))
|
|
1312
1316
|
Router.current // the active UIScreen (getter)
|
|
1313
|
-
Router.hide() / Router.restore() // hide/restore the whole router — stack stays alive; fires the top screen's onClose/onOpen
|
|
1317
|
+
Router.hide(opts?) / Router.restore(opts?) // hide/restore the whole router — stack stays alive; fires the top screen's onClose/onOpen
|
|
1318
|
+
// TRANSITIONS — every call above, and open() / close() of any destination, takes { transition }:
|
|
1319
|
+
// a name: "push" | "pop" | "slide-from-left" | "slide-from-right" | "slide-from-top" | "slide-from-bottom" |
|
|
1320
|
+
// "zoom" | "zoom-in" | "zoom-out" | "fade" | "none"
|
|
1321
|
+
// or your own: { enter?: pose, exit?: pose, onTop?: "enter" | "exit" } — the incoming screen comes FROM `enter`,
|
|
1322
|
+
// the outgoing one goes TO `exit`. pose = { transform?, opacity?, dim?, duration? (ms, 300), delay?, easing? };
|
|
1323
|
+
// arrays are keyframes; in a pose a % in translate is a percent of the screen ("translateY(100%)").
|
|
1324
|
+
// e.g. Router.push(page, { transition: { enter: { transform: "translateY(100%)", duration: 400 }, exit: { dim: 0.3 } } })
|
|
1325
|
+
// popTransition = the way BACK the screen remembers (Router.pop(), the back gesture, the system's back play it).
|
|
1326
|
+
// Default: the transition it came with, the other way round (a slide from the bottom leaves downwards;
|
|
1327
|
+
// a zoom / a fade go back with a fade). A replace without one keeps the way back of the screen it replaces.
|
|
1328
|
+
// A screen's own transform / opacity are not seen while a transition moves it — style a child instead.
|
|
1314
1329
|
Router.addEventListener("change", (screen: UIScreen) => ...) / .removeEventListener("change", cb) // after every navigation
|
|
1315
1330
|
// Screens BELOW the top stay mounted — element trees and state survive, they're just not rendered.
|
|
1316
1331
|
// push fires the outgoing screen's onClose + the incoming one's onOpen; pop fires them in reverse and
|
|
@@ -1332,7 +1347,7 @@ font("ibm-plex-sans", { weights: [400, 600], italic: true }) // narrow/extend t
|
|
|
1332
1347
|
// A project's own file: font("./fonts/Brand.ttf") → real family name read from the file itself
|
|
1333
1348
|
// (weight/italic too — no options); two weight files of one family return the same name.
|
|
1334
1349
|
// registerFont(family, url, {weight, style}) survives ONLY for runtime-computed URLs — rare.
|
|
1335
|
-
//
|
|
1350
|
+
// The app's font: theme({ fontFamily }). A second family is set per UIText (extract a shared Style<UIText>).
|
|
1336
1351
|
|
|
1337
1352
|
// ===== SVG IMAGES =====
|
|
1338
1353
|
// SvgSource wraps raw SVG XML for use as an image source: UIImage(SvgSource(`<svg ...>`)).
|
|
@@ -1434,17 +1449,16 @@ screen.open()
|
|
|
1434
1449
|
</file>
|
|
1435
1450
|
|
|
1436
1451
|
// === EXAMPLE 2: Theme tokens + fetched list with loading / error states ===
|
|
1437
|
-
// A tokens module every screen imports, light-themed
|
|
1438
|
-
//
|
|
1452
|
+
// A tokens module every screen imports, light-themed. ONE theme() call: the roles (bg, surface,
|
|
1453
|
+
// border, text, textMuted, accent) color the screens, the default text and the tab bar — no
|
|
1454
|
+
// color: "#111" on every label — and the app's own keys sit beside them.
|
|
1439
1455
|
<file name="tokens.ts">
|
|
1440
|
-
const palette = {
|
|
1441
|
-
bg: "#F4F6F5", card: "#FFFFFF", border: "#E4E8E6",
|
|
1442
|
-
text: "#131A17", muted: "#606B65",
|
|
1443
|
-
accent: "#15A34A", accentSoft: "#E7F6ED", onAccent: "#FFFFFF",
|
|
1444
|
-
}
|
|
1445
|
-
theme({ color: palette.text, primaryColor: palette.accent })
|
|
1446
1456
|
// Accessors ARE "var(--x)" strings — re-calling theme() with new values restyles the live app.
|
|
1447
|
-
export const colors
|
|
1457
|
+
export const colors = theme({
|
|
1458
|
+
bg: "#F4F6F5", surface: "#FFFFFF", border: "#E4E8E6",
|
|
1459
|
+
text: "#131A17", textMuted: "#606B65", accent: "#15A34A",
|
|
1460
|
+
accentSoft: "#E7F6ED", onAccent: "#FFFFFF",
|
|
1461
|
+
})
|
|
1448
1462
|
export const font = { // type scale — spread into styles: .style({ ...font.h2 })
|
|
1449
1463
|
h2: { fontSize: 22, fontWeight: 700 }, bodyStrong: { fontSize: 16, fontWeight: 600 },
|
|
1450
1464
|
small: { fontSize: 14 }, tiny: { fontSize: 12, fontWeight: 500 },
|
|
@@ -1461,9 +1475,9 @@ const Row = (u: User) => UIRow(
|
|
|
1461
1475
|
justifyContent: "center", alignItems: "center" }),
|
|
1462
1476
|
UIColumn(
|
|
1463
1477
|
UIText(u.name).style({ ...font.bodyStrong }),
|
|
1464
|
-
UIText(u.email).style({ ...font.small, color: colors.
|
|
1478
|
+
UIText(u.email).style({ ...font.small, color: colors.textMuted }),
|
|
1465
1479
|
).style({ flexGrow: 1, flexShrink: 1, gap: 2, alignItems: "flex-start" }),
|
|
1466
|
-
).style({ alignItems: "center", gap: 12, bgColor: colors.
|
|
1480
|
+
).style({ alignItems: "center", gap: 12, bgColor: colors.surface, borderRadius: 16,
|
|
1467
1481
|
border: `1px solid ${colors.border}`, p: 14 })
|
|
1468
1482
|
|
|
1469
1483
|
const Centered = (...children: UINodeChild[]) =>
|
|
@@ -1472,7 +1486,7 @@ const Centered = (...children: UINodeChild[]) =>
|
|
|
1472
1486
|
let body: UIColumn
|
|
1473
1487
|
|
|
1474
1488
|
const loadData = async () => {
|
|
1475
|
-
body.setContent([Centered(UIText("Loading…").style({ color: colors.
|
|
1489
|
+
body.setContent([Centered(UIText("Loading…").style({ color: colors.textMuted }))])
|
|
1476
1490
|
const res = await fetch("https://jsonplaceholder.typicode.com/users")
|
|
1477
1491
|
if (res.status !== 200) {
|
|
1478
1492
|
body.setContent([Centered(
|
package/prompts/dist/ar-app.md
CHANGED
|
@@ -813,8 +813,8 @@ Router.init(tabs) // UITabs IS a UIScreen — present it directly
|
|
|
813
813
|
// .select(id), .tab (getter), .onSelect(cb(id, i)) — fires on a bar tap, swipe, or select()
|
|
814
814
|
// .badge(id, value) — true = dot, number/string = count pill, false/null/0 clears
|
|
815
815
|
// .pager — the UIPager underneath; UIPager.push(detail) from any screen keeps the bar
|
|
816
|
-
//
|
|
817
|
-
//
|
|
816
|
+
// The bar is colored by the theme's roles (accent, textMuted, surface, border) — no keys of its
|
|
817
|
+
// own; one bar's own colors: tabs.bar.theme({ accent, surface }). Custom layout: UIPager below.
|
|
818
818
|
|
|
819
819
|
// UIPager — the navigation primitive under UITabs: sibling tabs that swipe natively, each tab its
|
|
820
820
|
// OWN push/pop stack. Reach for it directly for a plain stack (one-screen pager) or a fully custom
|
|
@@ -1030,7 +1030,7 @@ const btn: Style<UIButton> = { bgColor: "#333", borderRadius: 12 }
|
|
|
1030
1030
|
// Padding: p, px, py, pt, pb, pl, pr (≡ padding, paddingHorizontal, paddingVertical, paddingTop, ...)
|
|
1031
1031
|
// Margin: m, mx, my, mt, mb, ml, mr — margins also accept "auto" (mx: "auto" centers a fixed-width element)
|
|
1032
1032
|
// Background (containers/screen/button/input/video; UIText/UIImage/UISpacer have bgColor only):
|
|
1033
|
-
// bgColor, bgImage (string | FetchResponse | File), bgSize ("cover"|"contain"|"tile"), bgGradient
|
|
1033
|
+
// bgColor, bgImage (string | FetchResponse | File | SvgSource), bgSize ("cover"|"contain"|"fill"|"tile"), bgGradient
|
|
1034
1034
|
// bgGradient: a CSS linear-gradient() or radial-gradient() string, e.g. "linear-gradient(to top, rgba(0,0,0,0.7), transparent)"
|
|
1035
1035
|
// or "radial-gradient(circle at 50% 40%, #7B2FF7, #0a0a1a)". Comma-separate several gradients to stack them (first = on top).
|
|
1036
1036
|
// Layers paint bottom-to-top: bgColor → bgImage → bgGradient (gradient over image = text scrim).
|
|
@@ -1057,23 +1057,27 @@ const btn: Style<UIButton> = { bgColor: "#333", borderRadius: 12 }
|
|
|
1057
1057
|
// Env keywords resolving to the device insets (notch, home indicator):
|
|
1058
1058
|
// "safe-top" | "safe-bottom" | "safe-left" | "safe-right"; "safe-all" — p / m shorthands only
|
|
1059
1059
|
// Comfort tokens — where content comfortably starts, never collapse to 0: max(safe-edge, knob) on clearance
|
|
1060
|
-
// insets (iOS notch/home indicator
|
|
1061
|
-
// knob ADDS instead. Knob defaults: 12 top,
|
|
1060
|
+
// insets (iOS notch/home indicator); past exact-height system BARS (Android status/nav bar) the
|
|
1061
|
+
// knob ADDS instead. Knob defaults: 12 top, 8 bottom, 16 horizontal (the page gutter):
|
|
1062
1062
|
// "comfort-top" | "comfort-bottom" | "comfort-left" | "comfort-right"
|
|
1063
1063
|
// "comfort-x" — px/mx | "comfort-y" — py/my | "comfort-all" — p / m
|
|
1064
1064
|
// Accepted on per-side padding & margin, position offsets (top/left/bottom/right), and inside calc()/min()/max():
|
|
1065
|
-
.style({ pb: "comfort-bottom" }) // bottom bar: 34 over a home indicator,
|
|
1065
|
+
.style({ pb: "comfort-bottom" }) // bottom bar: 34 over a home indicator, 56 above Android's button bar, 8 on inset-less devices
|
|
1066
1066
|
.style({ px: "comfort-x" }) // page gutter that also clears the notch in landscape
|
|
1067
1067
|
.style({ pb: "calc(comfort-bottom + 56px)" }) // scroll content clearing a 56px floating bar
|
|
1068
1068
|
|
|
1069
1069
|
// --- Theme variables ---
|
|
1070
1070
|
// theme({...}) merges app vars into one table; styles read them with "var(--name)" (numbers are px
|
|
1071
1071
|
// lengths, null removes a key). Re-calling re-styles the LIVE UI — dark mode is a second call.
|
|
1072
|
-
// Returned accessors ARE the var() strings
|
|
1073
|
-
const T = theme({
|
|
1074
|
-
label.style({ color: T.
|
|
1075
|
-
//
|
|
1076
|
-
//
|
|
1072
|
+
// ONE call per app. Returned accessors ARE the var() strings:
|
|
1073
|
+
const T = theme({ accent: "#0A84FF", "comfort-left": 20 })
|
|
1074
|
+
label.style({ color: T.accent, pl: "var(--comfort-left)" })
|
|
1075
|
+
// ROLES — keys the SDK reads itself; use them as your palette and add your own keys beside them:
|
|
1076
|
+
// bg (every screen's background) · surface (cards, bars; the tab bar) · border (hairlines)
|
|
1077
|
+
// text (text with no color of its own) · textMuted (secondary; inactive tabs) · accent (active tab)
|
|
1078
|
+
// fontFamily (text with no font of its own). Light theme = set text once, not a color per label.
|
|
1079
|
+
// SCOPE: el.theme({...}) — the same keys for ONE subtree (a card, tabs.bar, a whole screen); its
|
|
1080
|
+
// descendants read them first. A pushed screen / a widget is its own root: give it its own.
|
|
1077
1081
|
// Bare comfort-* tokens apply the safe-area formula; "var(--comfort-top)" reads the raw knob.
|
|
1078
1082
|
// "var(--name, fallback)" applies the fallback while the key is unset. Env names (safe-*, vw/vh) are not theme keys.
|
|
1079
1083
|
|
|
@@ -1105,7 +1109,7 @@ screen.style({ flexDirection: "column", p: 16, $landscape: { flexDirection: "row
|
|
|
1105
1109
|
// alignItems: "stretch" — children FILL the cross axis by default; set a size or alignSelf to opt out.
|
|
1106
1110
|
// overflow: "hidden" — children are clipped to the parent's box; set overflow: "visible" to let them escape.
|
|
1107
1111
|
// boxSizing: "border-box" — width/height include padding & border.
|
|
1108
|
-
// Screen bg is black, default text is WHITE at fontSize 14 (retarget once via theme({
|
|
1112
|
+
// Screen bg is black, default text is WHITE at fontSize 14 (retarget once via theme({ bg, text, fontFamily })).
|
|
1109
1113
|
// On any light surface — white cards, sheets, inputs — white text renders invisible: use dark text there
|
|
1110
1114
|
// ("#111"/black), checking every UIText against the surface it actually sits on, not the screen.
|
|
1111
1115
|
|
|
@@ -1179,14 +1183,25 @@ UIButton().onLayout(({ width }) => { buttonWidth = width })
|
|
|
1179
1183
|
// ===== ROUTER (multi-page apps) =====
|
|
1180
1184
|
// Tabs are UITabs' job, in-tab stacks UIPager's (see UI COMPONENTS) — the Router owns what sits ABOVE the shell.
|
|
1181
1185
|
Router.init(homeScreen, opts?: { showDefaultBackButton?: boolean }) // call once in the entry file (default false)
|
|
1182
|
-
Router.push(screen)
|
|
1183
|
-
Router.pop(to?: number
|
|
1186
|
+
Router.push(screen, opts?: { transition, popTransition }) // push onto the stack, screen becomes active (default "push")
|
|
1187
|
+
Router.pop(to?: number, opts?: { transition })
|
|
1188
|
+
// default -1 = one back; negative = relative (-2 = back two), 0/positive = absolute
|
|
1184
1189
|
// stack index (0 = home). Popped screens' native trees are destroyed; the UIScreen
|
|
1185
|
-
// object stays reusable (push remounts it).
|
|
1186
|
-
Router.replace(screen, opts?: { transition }) // swap the top screen
|
|
1187
|
-
// "slide-from-right" | "slide-from-top" | "slide-from-bottom" | "zoom" | "zoom-out" | "zoom-in" | "fade" | "none" (default "fade")
|
|
1190
|
+
// object stays reusable (push remounts it). Plays the way back the screen remembers.
|
|
1191
|
+
Router.replace(screen, opts?: { transition, popTransition }) // swap the top screen (default "none"; theme({ replaceTransition }))
|
|
1188
1192
|
Router.current // the active UIScreen (getter)
|
|
1189
|
-
Router.hide() / Router.restore() // hide/restore the whole router — stack stays alive; fires the top screen's onClose/onOpen
|
|
1193
|
+
Router.hide(opts?) / Router.restore(opts?) // hide/restore the whole router — stack stays alive; fires the top screen's onClose/onOpen
|
|
1194
|
+
// TRANSITIONS — every call above, and open() / close() of any destination, takes { transition }:
|
|
1195
|
+
// a name: "push" | "pop" | "slide-from-left" | "slide-from-right" | "slide-from-top" | "slide-from-bottom" |
|
|
1196
|
+
// "zoom" | "zoom-in" | "zoom-out" | "fade" | "none"
|
|
1197
|
+
// or your own: { enter?: pose, exit?: pose, onTop?: "enter" | "exit" } — the incoming screen comes FROM `enter`,
|
|
1198
|
+
// the outgoing one goes TO `exit`. pose = { transform?, opacity?, dim?, duration? (ms, 300), delay?, easing? };
|
|
1199
|
+
// arrays are keyframes; in a pose a % in translate is a percent of the screen ("translateY(100%)").
|
|
1200
|
+
// e.g. Router.push(page, { transition: { enter: { transform: "translateY(100%)", duration: 400 }, exit: { dim: 0.3 } } })
|
|
1201
|
+
// popTransition = the way BACK the screen remembers (Router.pop(), the back gesture, the system's back play it).
|
|
1202
|
+
// Default: the transition it came with, the other way round (a slide from the bottom leaves downwards;
|
|
1203
|
+
// a zoom / a fade go back with a fade). A replace without one keeps the way back of the screen it replaces.
|
|
1204
|
+
// A screen's own transform / opacity are not seen while a transition moves it — style a child instead.
|
|
1190
1205
|
Router.addEventListener("change", (screen: UIScreen) => ...) / .removeEventListener("change", cb) // after every navigation
|
|
1191
1206
|
// Screens BELOW the top stay mounted — element trees and state survive, they're just not rendered.
|
|
1192
1207
|
// push fires the outgoing screen's onClose + the incoming one's onOpen; pop fires them in reverse and
|
|
@@ -1208,7 +1223,7 @@ font("ibm-plex-sans", { weights: [400, 600], italic: true }) // narrow/extend t
|
|
|
1208
1223
|
// A project's own file: font("./fonts/Brand.ttf") → real family name read from the file itself
|
|
1209
1224
|
// (weight/italic too — no options); two weight files of one family return the same name.
|
|
1210
1225
|
// registerFont(family, url, {weight, style}) survives ONLY for runtime-computed URLs — rare.
|
|
1211
|
-
//
|
|
1226
|
+
// The app's font: theme({ fontFamily }). A second family is set per UIText (extract a shared Style<UIText>).
|
|
1212
1227
|
|
|
1213
1228
|
// ===== SVG IMAGES =====
|
|
1214
1229
|
// SvgSource wraps raw SVG XML for use as an image source: UIImage(SvgSource(`<svg ...>`)).
|
|
@@ -1310,17 +1325,16 @@ screen.open()
|
|
|
1310
1325
|
</file>
|
|
1311
1326
|
|
|
1312
1327
|
// === EXAMPLE 2: Theme tokens + fetched list with loading / error states ===
|
|
1313
|
-
// A tokens module every screen imports, light-themed
|
|
1314
|
-
//
|
|
1328
|
+
// A tokens module every screen imports, light-themed. ONE theme() call: the roles (bg, surface,
|
|
1329
|
+
// border, text, textMuted, accent) color the screens, the default text and the tab bar — no
|
|
1330
|
+
// color: "#111" on every label — and the app's own keys sit beside them.
|
|
1315
1331
|
<file name="tokens.ts">
|
|
1316
|
-
const palette = {
|
|
1317
|
-
bg: "#F4F6F5", card: "#FFFFFF", border: "#E4E8E6",
|
|
1318
|
-
text: "#131A17", muted: "#606B65",
|
|
1319
|
-
accent: "#15A34A", accentSoft: "#E7F6ED", onAccent: "#FFFFFF",
|
|
1320
|
-
}
|
|
1321
|
-
theme({ color: palette.text, primaryColor: palette.accent })
|
|
1322
1332
|
// Accessors ARE "var(--x)" strings — re-calling theme() with new values restyles the live app.
|
|
1323
|
-
export const colors
|
|
1333
|
+
export const colors = theme({
|
|
1334
|
+
bg: "#F4F6F5", surface: "#FFFFFF", border: "#E4E8E6",
|
|
1335
|
+
text: "#131A17", textMuted: "#606B65", accent: "#15A34A",
|
|
1336
|
+
accentSoft: "#E7F6ED", onAccent: "#FFFFFF",
|
|
1337
|
+
})
|
|
1324
1338
|
export const font = { // type scale — spread into styles: .style({ ...font.h2 })
|
|
1325
1339
|
h2: { fontSize: 22, fontWeight: 700 }, bodyStrong: { fontSize: 16, fontWeight: 600 },
|
|
1326
1340
|
small: { fontSize: 14 }, tiny: { fontSize: 12, fontWeight: 500 },
|
|
@@ -1337,9 +1351,9 @@ const Row = (u: User) => UIRow(
|
|
|
1337
1351
|
justifyContent: "center", alignItems: "center" }),
|
|
1338
1352
|
UIColumn(
|
|
1339
1353
|
UIText(u.name).style({ ...font.bodyStrong }),
|
|
1340
|
-
UIText(u.email).style({ ...font.small, color: colors.
|
|
1354
|
+
UIText(u.email).style({ ...font.small, color: colors.textMuted }),
|
|
1341
1355
|
).style({ flexGrow: 1, flexShrink: 1, gap: 2, alignItems: "flex-start" }),
|
|
1342
|
-
).style({ alignItems: "center", gap: 12, bgColor: colors.
|
|
1356
|
+
).style({ alignItems: "center", gap: 12, bgColor: colors.surface, borderRadius: 16,
|
|
1343
1357
|
border: `1px solid ${colors.border}`, p: 14 })
|
|
1344
1358
|
|
|
1345
1359
|
const Centered = (...children: UINodeChild[]) =>
|
|
@@ -1348,7 +1362,7 @@ const Centered = (...children: UINodeChild[]) =>
|
|
|
1348
1362
|
let body: UIColumn
|
|
1349
1363
|
|
|
1350
1364
|
const loadData = async () => {
|
|
1351
|
-
body.setContent([Centered(UIText("Loading…").style({ color: colors.
|
|
1365
|
+
body.setContent([Centered(UIText("Loading…").style({ color: colors.textMuted }))])
|
|
1352
1366
|
const res = await fetch("https://jsonplaceholder.typicode.com/users")
|
|
1353
1367
|
if (res.status !== 200) {
|
|
1354
1368
|
body.setContent([Centered(
|