lecodes-sdk 0.20.0 → 0.20.2
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/dist/global.d.ts +31 -0
- package/dist/inject.js +361 -260
- package/dist/types/audio/Bus.d.ts +45 -0
- package/dist/types/audio/Sound.d.ts +28 -0
- package/dist/types/audio/Voice.d.ts +27 -0
- package/dist/types/audio/audio.d.ts +83 -0
- package/dist/types/audio/support.d.ts +1 -0
- package/dist/types/gl/AudioSource.d.ts +60 -0
- package/dist/types/gl/AudioZone.d.ts +32 -0
- package/dist/types/gl/DecalSet.d.ts +103 -0
- package/dist/types/gl/Geometry.d.ts +5 -0
- package/dist/types/gl/Light.d.ts +7 -0
- package/dist/types/gl/Material.d.ts +86 -2
- package/dist/types/gl/Mesh.d.ts +11 -0
- package/dist/types/gl/Scene.d.ts +23 -0
- package/dist/types/gl/SceneAudio.d.ts +11 -0
- package/dist/types/gl/Texture.d.ts +29 -1
- package/dist/types/gl/animation/AnimationClip.d.ts +25 -12
- package/dist/types/gl/animation/core.d.ts +15 -10
- package/dist/types/gl/state.d.ts +0 -1
- package/dist/types/inject.d.ts +10 -0
- package/dist/types/runtime/input.d.ts +11 -0
- package/dist/types/ui/UIImage.d.ts +15 -5
- package/dist/types.json +1 -1
- package/package.json +1 -1
- package/prompts/dist/2d-game.md +408 -197
- package/prompts/dist/3d-app.md +491 -166
- package/prompts/dist/ar-app.md +373 -163
- package/prompts/dist/design.md +83 -87
- package/prompts/dist/ui-app.md +325 -136
- package/src/audio/Bus.ts +102 -0
- package/src/audio/Sound.ts +96 -0
- package/src/audio/Voice.ts +102 -0
- package/src/audio/audio.ts +161 -0
- package/src/audio/support.ts +6 -0
- package/src/bridges.d.ts +1481 -1352
- package/src/compile/compileProject.ts +30 -15
- package/src/compile/index.ts +3 -0
- package/src/core/Aspect.ts +33 -8
- package/src/g2/Scene2D.ts +7 -0
- package/src/gl/AudioSource.ts +113 -0
- package/src/gl/AudioZone.ts +75 -0
- package/src/gl/CameraPlace.ts +52 -52
- package/src/gl/DecalSet.ts +233 -0
- package/src/gl/Geometry.ts +5 -0
- package/src/gl/Light.ts +16 -0
- package/src/gl/Lightmap.ts +3 -2
- package/src/gl/Material.ts +152 -4
- package/src/gl/Mesh.ts +20 -1
- package/src/gl/Ragdoll.ts +270 -270
- package/src/gl/Scene.ts +41 -7
- package/src/gl/SceneAudio.ts +26 -0
- package/src/gl/Texture.ts +43 -3
- package/src/gl/Trigger.ts +45 -45
- package/src/gl/Vehicle.ts +5 -5
- package/src/gl/animation/AnimationClip.ts +43 -20
- package/src/gl/animation/Animator.ts +4 -3
- package/src/gl/animation/core.ts +20 -15
- package/src/gl/scenarios.ts +291 -291
- package/src/gl/state.ts +1 -1
- package/src/inject.ts +12 -0
- package/src/runtime/input.ts +6 -1
- package/src/scene/gizmos.ts +148 -148
- package/src/ui/UIImage.ts +21 -7
package/prompts/dist/design.md
CHANGED
|
@@ -62,7 +62,9 @@ The platform runs the conversation in one of three modes the user switches betwe
|
|
|
62
62
|
|
|
63
63
|
<mode>build</mode> (or <mode>concept</mode>)
|
|
64
64
|
|
|
65
|
-
|
|
65
|
+
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.
|
|
66
|
+
|
|
67
|
+
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
68
|
|
|
67
69
|
## Important
|
|
68
70
|
|
|
@@ -82,21 +84,24 @@ The platform renders it as a "Switch to …" button — the user decides; nothin
|
|
|
82
84
|
|
|
83
85
|
// ===== UI COMPONENTS =====
|
|
84
86
|
// 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.
|
|
87
|
+
// CONTENT — children as plain arguments (or text/src/...). Everything else — styles — is configured
|
|
88
|
+
// by chaining: every configuring method returns the element itself, so construction reads as one chain.
|
|
89
|
+
UIColumn(UIText("Title"), UIButton(UIText("Go")))
|
|
90
|
+
// An ARRAY argument is flattened into the children — pass items.map(Row) directly, no spread:
|
|
91
|
+
UIColumn(header, items.map(Row), footer)
|
|
87
92
|
|
|
88
93
|
// UIRow, UIColumn — containers (UIColumn stacks vertically, UIRow horizontally)
|
|
89
|
-
UIRow(children) / UIColumn(children)
|
|
94
|
+
UIRow(...children) / UIColumn(...children)
|
|
90
95
|
|
|
91
96
|
// UIScreen — root screen, always fills the device. Behaves as a UIColumn.
|
|
92
|
-
UIScreen(children)
|
|
97
|
+
UIScreen(...children)
|
|
93
98
|
// Screens NEVER scroll — one vertical flow (article, long form, feed) = fixed chrome + ONE UIScrollable body
|
|
94
|
-
// with flexGrow: 1: UIScreen(
|
|
99
|
+
// with flexGrow: 1: UIScreen(Header(), UIScrollable(content).style({ flexGrow: 1 }))
|
|
95
100
|
// Note: a screen always fills the device — sizing styles on it (width, height, flexGrow, position) are no-ops
|
|
96
101
|
// A design screen is returned from the file's default export — never call .open()
|
|
97
102
|
|
|
98
103
|
// UIWidget — floating overlay above the screen, position: fixed in device coordinates
|
|
99
|
-
UIWidget(children)
|
|
104
|
+
UIWidget(...children)
|
|
100
105
|
// .show(), .hide(), .onOverlayTap(cb)
|
|
101
106
|
// extra style: overlayColor — full-screen scrim BEHIND the widget that blocks taps underneath, turning it
|
|
102
107
|
// into a modal (dialog / bottom sheet). Size the widget box to the CONTENT only (bottom-anchor a sheet
|
|
@@ -108,7 +113,7 @@ UIWidget(children)
|
|
|
108
113
|
|
|
109
114
|
// UIScrollable — THE scroll container: a screen's scrolling body, a list under a pinned header, a
|
|
110
115
|
// horizontal chip row / carousel
|
|
111
|
-
UIScrollable(children)
|
|
116
|
+
UIScrollable(...children)
|
|
112
117
|
// extra styles: scrollDirection ("horizontal" | "vertical", default vertical), showScrollbar: boolean
|
|
113
118
|
// Note: defaults flexShrink: 1 (scrolls instead of overflowing) — wrapping ancestors still need flexShrink: 1
|
|
114
119
|
|
|
@@ -127,7 +132,7 @@ UIImage(src) // src: string url | SvgSource | imported asset
|
|
|
127
132
|
|
|
128
133
|
// UIButton — the only TAPPABLE container: a UIRow with children centered on both axes by default.
|
|
129
134
|
// Anything clickable in the prototype (a card, a list row, an icon) — wrap it in a UIButton.
|
|
130
|
-
UIButton(children
|
|
135
|
+
UIButton(...children)
|
|
131
136
|
// extra styles: onPressed: { bgColor, opacity, ... } — style while the finger is down (press feedback is
|
|
132
137
|
// OPT-IN; set it so the live prototype feels real)
|
|
133
138
|
// Note: buttons render no chrome of their own — style bgColor/borderRadius/padding yourself, and give a
|
|
@@ -140,7 +145,7 @@ UIButton(children?)
|
|
|
140
145
|
|
|
141
146
|
// UISpacer — flexible empty space (defaults flexGrow: 1), eats free space along the main axis.
|
|
142
147
|
// Only when plain alignment can't express it (one item pushed to the far end while the rest stay put):
|
|
143
|
-
UIRow(
|
|
148
|
+
UIRow(title, UISpacer(), closeButton)
|
|
144
149
|
// If ALL children move together, justifyContent ("space-between", "flex-end", ...) does it with no extra element.
|
|
145
150
|
|
|
146
151
|
// ===== STYLING =====
|
|
@@ -197,7 +202,7 @@ const label: Style<UIText> = { fontSize: 12, fontWeight: 600, letterSpacing: 2 }
|
|
|
197
202
|
const T = theme({ primaryColor: "#0A84FF" })
|
|
198
203
|
label.style({ color: T.primaryColor }) // or "var(--primaryColor)"
|
|
199
204
|
// theme({ color, fontFamily }) drive the DEFAULT text color/font app-wide. "var(--name, fallback)"
|
|
200
|
-
// applies the fallback while the key is unset — how
|
|
205
|
+
// applies the fallback while the key is unset — how the SDK tab bar (defineTabs) stays themeable.
|
|
201
206
|
|
|
202
207
|
// --- Defaults that surprise ---
|
|
203
208
|
// flexShrink: 0 — elements don't shrink to fit (exception: UIScrollable defaults flexShrink: 1 so it scrolls
|
|
@@ -218,14 +223,14 @@ label.style({ color: T.primaryColor }) // or "var(--primaryColor)"
|
|
|
218
223
|
// ===== REUSABLE COMPONENTS =====
|
|
219
224
|
// Extract repeated UI into factory functions — they return elements you can chain on:
|
|
220
225
|
const StatusPill = (label: string, color: string, filled = false) =>
|
|
221
|
-
UIColumn(
|
|
226
|
+
UIColumn(UIText(label).style({ fontSize: 12, fontWeight: 600, color: filled ? "#05201f" : color }))
|
|
222
227
|
.style({ height: 26, px: 10, borderRadius: 999, justifyContent: "center", alignItems: "center",
|
|
223
228
|
bgColor: filled ? color : "transparent", border: filled ? undefined : `1px solid ${color}`,
|
|
224
229
|
alignSelf: "flex-start" })
|
|
225
230
|
|
|
226
231
|
// ===== CONDITIONAL CHILDREN =====
|
|
227
|
-
// null / undefined / false
|
|
228
|
-
UIColumn(
|
|
232
|
+
// A null / undefined / false child is skipped — no element, no layout slot.
|
|
233
|
+
UIColumn(header, item.unread ? dot : null, items.map(Card))
|
|
229
234
|
|
|
230
235
|
// ===== SIZING: the two axes behave differently =====
|
|
231
236
|
// MAIN axis (row → width, column → height): elements stay as small as their content — nothing grows
|
|
@@ -253,14 +258,14 @@ lineHeight: 1.5 // WRONG — number is px (=1.5px); for a m
|
|
|
253
258
|
// A wrapping container between the scrollable and the screen is missing flexShrink: 1
|
|
254
259
|
|
|
255
260
|
// ❌ empty containers as spacers to align children (web habit)
|
|
256
|
-
UIRow(
|
|
261
|
+
UIRow(UIColumn().style({ flexGrow: 1 }), label) // WRONG
|
|
257
262
|
// ✅ alignment is a CONTAINER property, not an extra element
|
|
258
|
-
UIRow(
|
|
263
|
+
UIRow(label).style({ justifyContent: "flex-end" })
|
|
259
264
|
|
|
260
265
|
// ❌ empty element as a placeholder for a conditional child
|
|
261
|
-
UIRow(
|
|
266
|
+
UIRow(isSelected ? check : UIColumn()) // WRONG
|
|
262
267
|
// ✅ null is skipped in children — no phantom element
|
|
263
|
-
UIRow(
|
|
268
|
+
UIRow(isSelected ? check : null)
|
|
264
269
|
|
|
265
270
|
// ❌ pointing UIImage / bgImage at a project file by bare path — it won't resolve to the bundled asset
|
|
266
271
|
UIImage("./photo.jpg") // WRONG (a plain string works only for remote http(s) URLs)
|
|
@@ -293,16 +298,27 @@ design/
|
|
|
293
298
|
A design that looks designed starts from a kit, not from screens. On a new design, before the first
|
|
294
299
|
screen: `shared/tokens.ts` (a real palette — surfaces, text tiers, one or two accents, status
|
|
295
300
|
colors; a spacing/radius/type scale), then `shared/ui.ts` with the components the app will clearly
|
|
296
|
-
need.
|
|
301
|
+
need. The kit outlives the design: when the app is built, it IMPORTS `shared/tokens.ts` (and the
|
|
302
|
+
presentational components of `shared/ui.ts`) unchanged — so build tokens on `theme()`, one live
|
|
303
|
+
table for design and app alike, and shape components as things worth keeping. Screens then read as
|
|
304
|
+
composition:
|
|
297
305
|
|
|
298
306
|
```ts
|
|
299
|
-
// design/shared/tokens.ts — surfaces dark-to-light, text in tiers, accents as a family
|
|
300
|
-
|
|
307
|
+
// design/shared/tokens.ts — surfaces dark-to-light, text in tiers, accents as a family.
|
|
308
|
+
// ONE theme() call registers the vars and returns the accessors ("var(--x)" strings); the system
|
|
309
|
+
// key `color` in the same table sets the app-wide default text color. Re-calling theme() with new
|
|
310
|
+
// values (dark mode, brand swap) restyles every screen — and later the built app — live.
|
|
311
|
+
const palette = {
|
|
301
312
|
bg: "#080808", card: "#161618", cardAlt: "#1d1d20", field: "#101012",
|
|
302
313
|
border: "#2a2a2e", text: "#ffffff", muted: "#8a8a93", faint: "#5a5b62",
|
|
303
314
|
accent: "#00BFC2", accentPressed: "#00999b", accentDeep: "#0c4e4f", onAccent: "#05201f",
|
|
304
315
|
success: "#30D158", danger: "#F83131", scrim: "#000000b3",
|
|
316
|
+
// theme SYSTEM keys — SDK-rendered chrome reads these. `color` = default text color everywhere;
|
|
317
|
+
// the other four brand the tab bar (defineTabs here, UITabs in the built app). Mirror the brand:
|
|
318
|
+
color: "#ffffff",
|
|
319
|
+
primaryColor: "#00BFC2", mutedColor: "#8a8a93", tabbarBg: "#161618", screenBg: "#080808",
|
|
305
320
|
}
|
|
321
|
+
export const colors: { [K in keyof typeof palette]: string } = theme(palette)
|
|
306
322
|
export const spacing = { xs: 4, sm: 8, md: 12, lg: 16, xl: 20, xxl: 24 }
|
|
307
323
|
export const radius = { sm: 8, md: 12, lg: 16, pill: 999 }
|
|
308
324
|
```
|
|
@@ -312,7 +328,7 @@ export const radius = { sm: 8, md: 12, lg: 16, pill: 999 }
|
|
|
312
328
|
import { colors, spacing, radius } from "./tokens"
|
|
313
329
|
|
|
314
330
|
export const PrimaryButton = (label: string, o: { name?: string, disabled?: boolean } = {}) =>
|
|
315
|
-
UIButton(
|
|
331
|
+
UIButton(UIText(label).style({ fontSize: 16, fontWeight: 700, color: o.disabled ? colors.muted : colors.onAccent }))
|
|
316
332
|
.style({ height: 52, borderRadius: radius.md, bgColor: o.disabled ? colors.accentDeep : colors.accent,
|
|
317
333
|
justifyContent: "center", alignItems: "center", name: o.name, onPressed: { bgColor: colors.accentPressed } })
|
|
318
334
|
|
|
@@ -321,15 +337,15 @@ export const SectionLabel = (t: string) =>
|
|
|
321
337
|
|
|
322
338
|
// A field is a STATIC MOCK — the value is drawn as text (see ui rules); passwords as dots.
|
|
323
339
|
export const Field = (o: { label?: string, value?: string, placeholder?: string, focused?: boolean }) =>
|
|
324
|
-
UIColumn(
|
|
340
|
+
UIColumn(
|
|
325
341
|
o.label ? SectionLabel(o.label) : null,
|
|
326
|
-
UIRow(
|
|
342
|
+
UIRow(UIText(o.value ?? o.placeholder ?? "").style({ flexGrow: 1, fontSize: 16, color: o.value ? colors.text : colors.muted }))
|
|
327
343
|
.style({ height: 52, px: 16, alignItems: "center", bgColor: colors.field, borderRadius: radius.md,
|
|
328
344
|
border: `1px solid ${o.focused ? colors.accent : colors.border}` }),
|
|
329
|
-
|
|
345
|
+
).style({ gap: spacing.sm })
|
|
330
346
|
|
|
331
347
|
// Bottom sheet shell — a real modal: the scrim is overlayColor, the box is just the bottom stack.
|
|
332
|
-
export const Sheet = (children: UINodeChild[]) => {
|
|
348
|
+
export const Sheet = (...children: UINodeChild[]) => {
|
|
333
349
|
const sheet = UIWidget(children).style({
|
|
334
350
|
left: 0, right: 0, bottom: 0, px: spacing.lg, pb: "calc(safe-bottom + 16px)", gap: spacing.sm,
|
|
335
351
|
overlayColor: colors.scrim,
|
|
@@ -339,19 +355,19 @@ export const Sheet = (children: UINodeChild[]) => {
|
|
|
339
355
|
|
|
340
356
|
// Full-screen confirm — full-inset box, centered card, NO onOverlayTap (a confirm demands a choice).
|
|
341
357
|
export const ConfirmDialog = (o: { title: string, body?: string, confirmLabel: string, confirmName: string }) =>
|
|
342
|
-
UIWidget(
|
|
343
|
-
UIColumn(
|
|
358
|
+
UIWidget(
|
|
359
|
+
UIColumn(
|
|
344
360
|
UIText(o.title).style({ fontSize: 17, fontWeight: 700, color: colors.text, textAlign: "center" }),
|
|
345
361
|
o.body ? UIText(o.body).style({ fontSize: 14, color: colors.muted, textAlign: "center", lineHeight: "1.4em" }) : null,
|
|
346
362
|
PrimaryButton("Cancel", { name: "cancel" }),
|
|
347
|
-
|
|
348
|
-
|
|
363
|
+
).style({ bgColor: colors.card, borderRadius: radius.lg, p: spacing.xl, gap: spacing.lg, width: "86%", maxWidth: 360 }),
|
|
364
|
+
).style({ top: 0, left: 0, right: 0, bottom: 0, justifyContent: "center", alignItems: "center", overlayColor: colors.scrim })
|
|
349
365
|
```
|
|
350
366
|
|
|
351
367
|
## Screen rules
|
|
352
368
|
|
|
353
369
|
- **One file = one screen.** `screens/<id>.ts` default-exports either a value
|
|
354
|
-
(`export default UIScreen(
|
|
370
|
+
(`export default UIScreen(...).style({...})`) or a function of state (below). Ids are
|
|
355
371
|
lowercase `[a-z0-9-]`.
|
|
356
372
|
- **Screens are islands.** A screen imports ONLY from `../shared/` — never another screen, no props.
|
|
357
373
|
- **Mock data is the domain.** Realistic inline mock data at the top of the file — the entities and
|
|
@@ -379,20 +395,20 @@ const classes = [
|
|
|
379
395
|
{ start: "19:30", end: "21:00", title: "Acrobatics workshop", trainer: "A. Razgulin", price: 1000, seats: 2 },
|
|
380
396
|
]
|
|
381
397
|
|
|
382
|
-
const FilterSheet = () => Sheet(
|
|
398
|
+
const FilterSheet = () => Sheet(
|
|
383
399
|
/* chip groups… */
|
|
384
400
|
PrimaryButton("Show 1 class", { name: "apply" }),
|
|
385
|
-
|
|
401
|
+
)
|
|
386
402
|
|
|
387
403
|
export default (state: "default" | "empty" | "filters" = "default") => {
|
|
388
404
|
if (state === "filters") FilterSheet().show()
|
|
389
|
-
return UIScreen(
|
|
390
|
-
UIScrollable(
|
|
405
|
+
return UIScreen(
|
|
406
|
+
UIScrollable(
|
|
391
407
|
SectionLabel("Today"),
|
|
392
408
|
state === "empty" ? EmptyDay() : UIColumn(classes.map((c) => ClassCard(c, { name: "class" }))).style({ gap: spacing.sm }),
|
|
393
|
-
|
|
409
|
+
).style({ flexGrow: 1, flexShrink: 1, px: spacing.xl, pt: spacing.md, gap: spacing.lg }),
|
|
394
410
|
mainTabs.bar("schedule"),
|
|
395
|
-
|
|
411
|
+
).style({ bgColor: colors.bg, pt: "safe-top" })
|
|
396
412
|
}
|
|
397
413
|
```
|
|
398
414
|
|
|
@@ -426,60 +442,40 @@ design_map ops: [
|
|
|
426
442
|
## The tab bar
|
|
427
443
|
|
|
428
444
|
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):
|
|
445
|
+
with lanes; tab switching needs NO edges. `defineTabs` is an SDK global: the standard themed
|
|
446
|
+
bottom bar, the same look the built app's `UITabs` renders. The keys are screen ids (each tab's
|
|
447
|
+
root, in tab order), literals only; icons are always `assetIcon(...)` calls:
|
|
437
448
|
|
|
438
449
|
```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
|
-
}
|
|
450
|
+
// design/shared/tabs.ts — this declaration is the whole file
|
|
451
|
+
export const mainTabs = defineTabs({
|
|
452
|
+
schedule: { label: "Schedule", icon: assetIcon("lucide:calendar") },
|
|
453
|
+
profile: { label: "Profile", icon: assetIcon("lucide:user") },
|
|
454
|
+
})
|
|
466
455
|
```
|
|
467
456
|
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
`
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
457
|
+
A screen that shows the bar mounts it with a literal tab id — roots AND pushed details that keep
|
|
458
|
+
the bar; a screen that doesn't mount it is a full-screen push. Mount the bar alone when the screen
|
|
459
|
+
owns its layout (`mainTabs.bar("schedule")`, as above) or wrap content with
|
|
460
|
+
`mainTabs.screen("home", [...])`. Restyle through the theme's system keys — `primaryColor`,
|
|
461
|
+
`mutedColor`, `tabbarBg`, `screenBg`, set in `shared/tokens.ts` — never by reimplementing the
|
|
462
|
+
bar. Keep the `defineTabs({...})` declaration and `.screen("<tab>", ...)` / `.bar("<tab>")` call
|
|
463
|
+
shapes intact — the board parses them.
|
|
464
|
+
|
|
465
|
+
## Icons & fonts
|
|
466
|
+
|
|
467
|
+
`assetIcon("pack:name")` inlines a registry icon at compile time — the ONLY way to make an icon
|
|
468
|
+
(never hand-write its `{ svg }` result — that renders blank). Use real icons freely on every
|
|
469
|
+
screen. Vendored copies (`design/assets/icons/`, listed under `[Assets]`) resolve first; anything
|
|
470
|
+
else is fetched from the icon registry automatically. The main pack is `lucide` (kebab-case names:
|
|
471
|
+
`"lucide:bell"`, `"lucide:chevron-right"`); an unknown name is a compile error with "did you mean"
|
|
472
|
+
suggestions. Wrap each icon in a tiny factory in `shared/ui.ts`
|
|
473
|
+
(`Icon.bell = (c, s = 20) => UIImage(assetIcon("lucide:bell")).style({ width: s, height: s, tintColor: c })`).
|
|
474
|
+
|
|
475
|
+
`font("id")` works the same way — registry families load from the font CDN, no vendoring needed:
|
|
476
|
+
`theme({ fontFamily: font("manrope") })`, or per-node `.style({ fontFamily: font("rubik") })`.
|
|
477
|
+
An unknown id or weight is a compile error listing what exists. Only project font files
|
|
478
|
+
(`font("./Brand.ttf")`) must already be pushed.
|
|
483
479
|
|
|
484
480
|
## spec.md — the concept
|
|
485
481
|
|