lecodes-sdk 1.0.0 → 1.2.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/dist/global.d.ts +18 -4
- package/dist/inject.js +260 -361
- package/dist/types/animate/tween/Animation.d.ts +69 -0
- package/dist/types/animate/tween/Timeline.d.ts +55 -0
- package/dist/types/animate/tween/animateValue.d.ts +27 -0
- package/dist/types/animate/tween/easing.d.ts +29 -0
- package/dist/types/animate/tween/spec.d.ts +178 -0
- package/dist/types/g2/Node2D.d.ts +16 -0
- package/dist/types/g2/Sprite.d.ts +11 -1
- package/dist/types/gl/Camera.d.ts +15 -1
- package/dist/types/gl/Foliage.d.ts +47 -0
- package/dist/types/gl/Geometry.d.ts +24 -0
- package/dist/types/gl/Light.d.ts +28 -7
- package/dist/types/gl/Lightmap.d.ts +90 -60
- package/dist/types/gl/Material.d.ts +28 -20
- package/dist/types/gl/Model.d.ts +7 -5
- package/dist/types/gl/Node.d.ts +18 -0
- package/dist/types/gl/Particles.d.ts +55 -1
- package/dist/types/gl/Scene.d.ts +20 -0
- package/dist/types/gl/animation/AnimationClip.d.ts +19 -0
- package/dist/types/gl/animation/Animator.d.ts +27 -0
- package/dist/types/gl/animation/DynamicBone.d.ts +19 -8
- package/dist/types/gl/animation/IK.d.ts +86 -30
- package/dist/types/gl/animation/Locomotion.d.ts +52 -3
- package/dist/types/gl/animation/Warp.d.ts +2 -1
- package/dist/types/gl/animation/core.d.ts +35 -4
- package/dist/types/gl/physics/Ragdoll.d.ts +87 -12
- package/dist/types/gl/terrain/Terrain.d.ts +12 -2
- package/dist/types/inject.d.ts +8 -2
- package/dist/types/runtime/input.d.ts +7 -0
- package/dist/types/scene/defineScene.d.ts +44 -32
- package/dist/types/ui/UIButton.d.ts +3 -1
- package/dist/types/ui/UIInput.d.ts +5 -1
- package/dist/types/ui/UINode.d.ts +24 -24
- package/dist/types.json +1 -1
- package/package.json +1 -1
- package/prompts/README.md +142 -142
- package/prompts/core-design.md +27 -4
- package/prompts/core.md +35 -6
- package/prompts/dist/2d-game.md +197 -408
- package/prompts/dist/3d-app.md +166 -491
- package/prompts/dist/ar-app.md +163 -373
- package/prompts/dist/design.md +87 -83
- package/prompts/dist/ui-app.md +136 -325
- package/prompts/select.ts +19 -4
- package/src/animate/tween/Animation.ts +378 -0
- package/src/animate/tween/Timeline.ts +175 -0
- package/src/animate/tween/animateValue.ts +100 -0
- package/src/animate/tween/easing.ts +172 -0
- package/src/animate/tween/spec.ts +479 -0
- package/src/audio/audio.ts +161 -161
- package/src/bridges.d.ts +235 -65
- package/src/compile/__tests__/assetMacro.test.ts +26 -0
- package/src/compile/__tests__/detectEntry.test.ts +19 -0
- package/src/compile/__tests__/serverSplit.test.ts +27 -0
- package/src/compile/bundler.ts +34 -4
- package/src/compile/compileProject.ts +31 -1
- package/src/compile/detectEntry.ts +8 -3
- package/src/compile/index.ts +2 -0
- package/src/compile/serverSplit.ts +9 -3
- package/src/g2/Node2D.ts +38 -0
- package/src/g2/Sprite.ts +20 -1
- package/src/gl/Camera.ts +34 -1
- package/src/gl/CameraPlace.ts +52 -52
- package/src/gl/Foliage.ts +102 -0
- package/src/gl/Geometry.ts +393 -348
- package/src/gl/Light.ts +49 -16
- package/src/gl/Lightmap.ts +439 -275
- package/src/gl/Material.ts +59 -47
- package/src/gl/Mesh.ts +120 -120
- package/src/gl/Model.ts +23 -12
- package/src/gl/Node.ts +39 -0
- package/src/gl/Particles.ts +80 -2
- package/src/gl/Scene.ts +34 -1
- package/src/gl/animation/AnimationClip.ts +52 -0
- package/src/gl/animation/Animator.ts +42 -2
- package/src/gl/animation/DynamicBone.ts +35 -12
- package/src/gl/animation/IK.ts +173 -152
- package/src/gl/animation/Locomotion.ts +72 -8
- package/src/gl/animation/Playback.ts +5 -4
- package/src/gl/animation/Warp.ts +5 -2
- package/src/gl/animation/core.ts +65 -4
- package/src/gl/physics/Ragdoll.ts +451 -272
- package/src/gl/scenarios.ts +291 -291
- package/src/gl/terrain/Terrain.ts +33 -2
- package/src/inject.ts +236 -226
- package/src/runtime/input.ts +11 -0
- package/src/scene/defineScene.ts +72 -62
- package/src/scene/gizmos.ts +148 -148
- package/src/ui/UIButton.ts +2 -2
- package/src/ui/UIInput.ts +3 -3
- package/src/ui/UINode.ts +61 -36
- package/dist/types/animate/animate.d.ts +0 -20
- package/src/animate/animate.ts +0 -238
package/prompts/dist/design.md
CHANGED
|
@@ -62,9 +62,7 @@ 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
|
-
|
|
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.
|
|
65
|
+
The platform renders it as a "Switch to …" button — the user decides; nothing switches by itself. At most one <mode> directive per reply, and never for normal design work.
|
|
68
66
|
|
|
69
67
|
## Important
|
|
70
68
|
|
|
@@ -84,24 +82,21 @@ The platform renders the directive as a button — the user decides; nothing swi
|
|
|
84
82
|
|
|
85
83
|
// ===== UI COMPONENTS =====
|
|
86
84
|
// Every element is created by a global factory function (never `new`) that takes only the element's
|
|
87
|
-
// CONTENT
|
|
88
|
-
//
|
|
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)
|
|
85
|
+
// CONTENT (children array, text, src, ...). Everything else — styles — is configured by chaining:
|
|
86
|
+
// every configuring method returns the element itself, so construction reads as one chain.
|
|
92
87
|
|
|
93
88
|
// UIRow, UIColumn — containers (UIColumn stacks vertically, UIRow horizontally)
|
|
94
|
-
UIRow(
|
|
89
|
+
UIRow(children) / UIColumn(children)
|
|
95
90
|
|
|
96
91
|
// UIScreen — root screen, always fills the device. Behaves as a UIColumn.
|
|
97
|
-
UIScreen(
|
|
92
|
+
UIScreen(children)
|
|
98
93
|
// Screens NEVER scroll — one vertical flow (article, long form, feed) = fixed chrome + ONE UIScrollable body
|
|
99
|
-
// with flexGrow: 1: UIScreen(Header(), UIScrollable(content).style({ flexGrow: 1 }))
|
|
94
|
+
// with flexGrow: 1: UIScreen([ Header(), UIScrollable([...content]).style({ flexGrow: 1 }) ])
|
|
100
95
|
// Note: a screen always fills the device — sizing styles on it (width, height, flexGrow, position) are no-ops
|
|
101
96
|
// A design screen is returned from the file's default export — never call .open()
|
|
102
97
|
|
|
103
98
|
// UIWidget — floating overlay above the screen, position: fixed in device coordinates
|
|
104
|
-
UIWidget(
|
|
99
|
+
UIWidget(children)
|
|
105
100
|
// .show(), .hide(), .onOverlayTap(cb)
|
|
106
101
|
// extra style: overlayColor — full-screen scrim BEHIND the widget that blocks taps underneath, turning it
|
|
107
102
|
// into a modal (dialog / bottom sheet). Size the widget box to the CONTENT only (bottom-anchor a sheet
|
|
@@ -113,7 +108,7 @@ UIWidget(...children)
|
|
|
113
108
|
|
|
114
109
|
// UIScrollable — THE scroll container: a screen's scrolling body, a list under a pinned header, a
|
|
115
110
|
// horizontal chip row / carousel
|
|
116
|
-
UIScrollable(
|
|
111
|
+
UIScrollable(children)
|
|
117
112
|
// extra styles: scrollDirection ("horizontal" | "vertical", default vertical), showScrollbar: boolean
|
|
118
113
|
// Note: defaults flexShrink: 1 (scrolls instead of overflowing) — wrapping ancestors still need flexShrink: 1
|
|
119
114
|
|
|
@@ -132,7 +127,7 @@ UIImage(src) // src: string url | SvgSource | imported asset
|
|
|
132
127
|
|
|
133
128
|
// UIButton — the only TAPPABLE container: a UIRow with children centered on both axes by default.
|
|
134
129
|
// Anything clickable in the prototype (a card, a list row, an icon) — wrap it in a UIButton.
|
|
135
|
-
UIButton(
|
|
130
|
+
UIButton(children?)
|
|
136
131
|
// extra styles: onPressed: { bgColor, opacity, ... } — style while the finger is down (press feedback is
|
|
137
132
|
// OPT-IN; set it so the live prototype feels real)
|
|
138
133
|
// Note: buttons render no chrome of their own — style bgColor/borderRadius/padding yourself, and give a
|
|
@@ -145,7 +140,7 @@ UIButton(...children)
|
|
|
145
140
|
|
|
146
141
|
// UISpacer — flexible empty space (defaults flexGrow: 1), eats free space along the main axis.
|
|
147
142
|
// Only when plain alignment can't express it (one item pushed to the far end while the rest stay put):
|
|
148
|
-
UIRow(title, UISpacer(), closeButton)
|
|
143
|
+
UIRow([ title, UISpacer(), closeButton ])
|
|
149
144
|
// If ALL children move together, justifyContent ("space-between", "flex-end", ...) does it with no extra element.
|
|
150
145
|
|
|
151
146
|
// ===== STYLING =====
|
|
@@ -202,7 +197,7 @@ const label: Style<UIText> = { fontSize: 12, fontWeight: 600, letterSpacing: 2 }
|
|
|
202
197
|
const T = theme({ primaryColor: "#0A84FF" })
|
|
203
198
|
label.style({ color: T.primaryColor }) // or "var(--primaryColor)"
|
|
204
199
|
// theme({ color, fontFamily }) drive the DEFAULT text color/font app-wide. "var(--name, fallback)"
|
|
205
|
-
// applies the fallback while the key is unset — how
|
|
200
|
+
// applies the fallback while the key is unset — how shared/tabs.ts stays themeable.
|
|
206
201
|
|
|
207
202
|
// --- Defaults that surprise ---
|
|
208
203
|
// flexShrink: 0 — elements don't shrink to fit (exception: UIScrollable defaults flexShrink: 1 so it scrolls
|
|
@@ -223,14 +218,14 @@ label.style({ color: T.primaryColor }) // or "var(--primaryColor)"
|
|
|
223
218
|
// ===== REUSABLE COMPONENTS =====
|
|
224
219
|
// Extract repeated UI into factory functions — they return elements you can chain on:
|
|
225
220
|
const StatusPill = (label: string, color: string, filled = false) =>
|
|
226
|
-
UIColumn(UIText(label).style({ fontSize: 12, fontWeight: 600, color: filled ? "#05201f" : color }))
|
|
221
|
+
UIColumn([ UIText(label).style({ fontSize: 12, fontWeight: 600, color: filled ? "#05201f" : color }) ])
|
|
227
222
|
.style({ height: 26, px: 10, borderRadius: 999, justifyContent: "center", alignItems: "center",
|
|
228
223
|
bgColor: filled ? color : "transparent", border: filled ? undefined : `1px solid ${color}`,
|
|
229
224
|
alignSelf: "flex-start" })
|
|
230
225
|
|
|
231
226
|
// ===== CONDITIONAL CHILDREN =====
|
|
232
|
-
//
|
|
233
|
-
UIColumn(header, item.unread ? dot : null, items.map(Card))
|
|
227
|
+
// null / undefined / false in a children array is skipped — no element, no layout slot.
|
|
228
|
+
UIColumn([ header, item.unread ? dot : null, ...items.map(Card) ])
|
|
234
229
|
|
|
235
230
|
// ===== SIZING: the two axes behave differently =====
|
|
236
231
|
// MAIN axis (row → width, column → height): elements stay as small as their content — nothing grows
|
|
@@ -258,14 +253,14 @@ lineHeight: 1.5 // WRONG — number is px (=1.5px); for a m
|
|
|
258
253
|
// A wrapping container between the scrollable and the screen is missing flexShrink: 1
|
|
259
254
|
|
|
260
255
|
// ❌ empty containers as spacers to align children (web habit)
|
|
261
|
-
UIRow(UIColumn().style({ flexGrow: 1 }), label) // WRONG
|
|
256
|
+
UIRow([ UIColumn([]).style({ flexGrow: 1 }), label ]) // WRONG
|
|
262
257
|
// ✅ alignment is a CONTAINER property, not an extra element
|
|
263
|
-
UIRow(label).style({ justifyContent: "flex-end" })
|
|
258
|
+
UIRow([ label ]).style({ justifyContent: "flex-end" })
|
|
264
259
|
|
|
265
260
|
// ❌ empty element as a placeholder for a conditional child
|
|
266
|
-
UIRow(isSelected ? check : UIColumn()) // WRONG
|
|
261
|
+
UIRow([ isSelected ? check : UIColumn([]) ]) // WRONG
|
|
267
262
|
// ✅ null is skipped in children — no phantom element
|
|
268
|
-
UIRow(isSelected ? check : null)
|
|
263
|
+
UIRow([ isSelected ? check : null ])
|
|
269
264
|
|
|
270
265
|
// ❌ pointing UIImage / bgImage at a project file by bare path — it won't resolve to the bundled asset
|
|
271
266
|
UIImage("./photo.jpg") // WRONG (a plain string works only for remote http(s) URLs)
|
|
@@ -298,27 +293,16 @@ design/
|
|
|
298
293
|
A design that looks designed starts from a kit, not from screens. On a new design, before the first
|
|
299
294
|
screen: `shared/tokens.ts` (a real palette — surfaces, text tiers, one or two accents, status
|
|
300
295
|
colors; a spacing/radius/type scale), then `shared/ui.ts` with the components the app will clearly
|
|
301
|
-
need.
|
|
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:
|
|
296
|
+
need. Screens then read as composition:
|
|
305
297
|
|
|
306
298
|
```ts
|
|
307
|
-
// design/shared/tokens.ts — surfaces dark-to-light, text in tiers, accents as a family
|
|
308
|
-
|
|
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 = {
|
|
299
|
+
// design/shared/tokens.ts — surfaces dark-to-light, text in tiers, accents as a family
|
|
300
|
+
export const colors = {
|
|
312
301
|
bg: "#080808", card: "#161618", cardAlt: "#1d1d20", field: "#101012",
|
|
313
302
|
border: "#2a2a2e", text: "#ffffff", muted: "#8a8a93", faint: "#5a5b62",
|
|
314
303
|
accent: "#00BFC2", accentPressed: "#00999b", accentDeep: "#0c4e4f", onAccent: "#05201f",
|
|
315
304
|
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",
|
|
320
305
|
}
|
|
321
|
-
export const colors: { [K in keyof typeof palette]: string } = theme(palette)
|
|
322
306
|
export const spacing = { xs: 4, sm: 8, md: 12, lg: 16, xl: 20, xxl: 24 }
|
|
323
307
|
export const radius = { sm: 8, md: 12, lg: 16, pill: 999 }
|
|
324
308
|
```
|
|
@@ -328,7 +312,7 @@ export const radius = { sm: 8, md: 12, lg: 16, pill: 999 }
|
|
|
328
312
|
import { colors, spacing, radius } from "./tokens"
|
|
329
313
|
|
|
330
314
|
export const PrimaryButton = (label: string, o: { name?: string, disabled?: boolean } = {}) =>
|
|
331
|
-
UIButton(UIText(label).style({ fontSize: 16, fontWeight: 700, color: o.disabled ? colors.muted : colors.onAccent }))
|
|
315
|
+
UIButton([ UIText(label).style({ fontSize: 16, fontWeight: 700, color: o.disabled ? colors.muted : colors.onAccent }) ])
|
|
332
316
|
.style({ height: 52, borderRadius: radius.md, bgColor: o.disabled ? colors.accentDeep : colors.accent,
|
|
333
317
|
justifyContent: "center", alignItems: "center", name: o.name, onPressed: { bgColor: colors.accentPressed } })
|
|
334
318
|
|
|
@@ -337,15 +321,15 @@ export const SectionLabel = (t: string) =>
|
|
|
337
321
|
|
|
338
322
|
// A field is a STATIC MOCK — the value is drawn as text (see ui rules); passwords as dots.
|
|
339
323
|
export const Field = (o: { label?: string, value?: string, placeholder?: string, focused?: boolean }) =>
|
|
340
|
-
UIColumn(
|
|
324
|
+
UIColumn([
|
|
341
325
|
o.label ? SectionLabel(o.label) : null,
|
|
342
|
-
UIRow(UIText(o.value ?? o.placeholder ?? "").style({ flexGrow: 1, fontSize: 16, color: o.value ? colors.text : colors.muted }))
|
|
326
|
+
UIRow([ UIText(o.value ?? o.placeholder ?? "").style({ flexGrow: 1, fontSize: 16, color: o.value ? colors.text : colors.muted }) ])
|
|
343
327
|
.style({ height: 52, px: 16, alignItems: "center", bgColor: colors.field, borderRadius: radius.md,
|
|
344
328
|
border: `1px solid ${o.focused ? colors.accent : colors.border}` }),
|
|
345
|
-
).style({ gap: spacing.sm })
|
|
329
|
+
]).style({ gap: spacing.sm })
|
|
346
330
|
|
|
347
331
|
// Bottom sheet shell — a real modal: the scrim is overlayColor, the box is just the bottom stack.
|
|
348
|
-
export const Sheet = (
|
|
332
|
+
export const Sheet = (children: UINodeChild[]) => {
|
|
349
333
|
const sheet = UIWidget(children).style({
|
|
350
334
|
left: 0, right: 0, bottom: 0, px: spacing.lg, pb: "calc(safe-bottom + 16px)", gap: spacing.sm,
|
|
351
335
|
overlayColor: colors.scrim,
|
|
@@ -355,19 +339,19 @@ export const Sheet = (...children: UINodeChild[]) => {
|
|
|
355
339
|
|
|
356
340
|
// Full-screen confirm — full-inset box, centered card, NO onOverlayTap (a confirm demands a choice).
|
|
357
341
|
export const ConfirmDialog = (o: { title: string, body?: string, confirmLabel: string, confirmName: string }) =>
|
|
358
|
-
UIWidget(
|
|
359
|
-
UIColumn(
|
|
342
|
+
UIWidget([
|
|
343
|
+
UIColumn([
|
|
360
344
|
UIText(o.title).style({ fontSize: 17, fontWeight: 700, color: colors.text, textAlign: "center" }),
|
|
361
345
|
o.body ? UIText(o.body).style({ fontSize: 14, color: colors.muted, textAlign: "center", lineHeight: "1.4em" }) : null,
|
|
362
346
|
PrimaryButton("Cancel", { name: "cancel" }),
|
|
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 })
|
|
347
|
+
]).style({ bgColor: colors.card, borderRadius: radius.lg, p: spacing.xl, gap: spacing.lg, width: "86%", maxWidth: 360 }),
|
|
348
|
+
]).style({ top: 0, left: 0, right: 0, bottom: 0, justifyContent: "center", alignItems: "center", overlayColor: colors.scrim })
|
|
365
349
|
```
|
|
366
350
|
|
|
367
351
|
## Screen rules
|
|
368
352
|
|
|
369
353
|
- **One file = one screen.** `screens/<id>.ts` default-exports either a value
|
|
370
|
-
(`export default UIScreen(...).style({...})`) or a function of state (below). Ids are
|
|
354
|
+
(`export default UIScreen([...]).style({...})`) or a function of state (below). Ids are
|
|
371
355
|
lowercase `[a-z0-9-]`.
|
|
372
356
|
- **Screens are islands.** A screen imports ONLY from `../shared/` — never another screen, no props.
|
|
373
357
|
- **Mock data is the domain.** Realistic inline mock data at the top of the file — the entities and
|
|
@@ -395,20 +379,20 @@ const classes = [
|
|
|
395
379
|
{ start: "19:30", end: "21:00", title: "Acrobatics workshop", trainer: "A. Razgulin", price: 1000, seats: 2 },
|
|
396
380
|
]
|
|
397
381
|
|
|
398
|
-
const FilterSheet = () => Sheet(
|
|
382
|
+
const FilterSheet = () => Sheet([
|
|
399
383
|
/* chip groups… */
|
|
400
384
|
PrimaryButton("Show 1 class", { name: "apply" }),
|
|
401
|
-
)
|
|
385
|
+
])
|
|
402
386
|
|
|
403
387
|
export default (state: "default" | "empty" | "filters" = "default") => {
|
|
404
388
|
if (state === "filters") FilterSheet().show()
|
|
405
|
-
return UIScreen(
|
|
406
|
-
UIScrollable(
|
|
389
|
+
return UIScreen([
|
|
390
|
+
UIScrollable([
|
|
407
391
|
SectionLabel("Today"),
|
|
408
392
|
state === "empty" ? EmptyDay() : UIColumn(classes.map((c) => ClassCard(c, { name: "class" }))).style({ gap: spacing.sm }),
|
|
409
|
-
).style({ flexGrow: 1, flexShrink: 1, px: spacing.xl, pt: spacing.md, gap: spacing.lg }),
|
|
393
|
+
]).style({ flexGrow: 1, flexShrink: 1, px: spacing.xl, pt: spacing.md, gap: spacing.lg }),
|
|
410
394
|
mainTabs.bar("schedule"),
|
|
411
|
-
).style({ bgColor: colors.bg, pt: "safe-top" })
|
|
395
|
+
]).style({ bgColor: colors.bg, pt: "safe-top" })
|
|
412
396
|
}
|
|
413
397
|
```
|
|
414
398
|
|
|
@@ -442,40 +426,60 @@ design_map ops: [
|
|
|
442
426
|
## The tab bar
|
|
443
427
|
|
|
444
428
|
Declared ONCE in `shared/tabs.ts` — the board discovers it from source and renders it as a rail
|
|
445
|
-
with lanes; tab switching needs NO edges. `defineTabs`
|
|
446
|
-
|
|
447
|
-
|
|
429
|
+
with lanes; tab switching needs NO edges. The `defineTabs` keys are screen ids (each tab's root, in
|
|
430
|
+
tab order), literals only. A screen that shows the bar mounts it with a literal tab id — roots AND
|
|
431
|
+
pushed details that keep the bar; a screen that doesn't mount it is a full-screen push. Mount the
|
|
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):
|
|
448
437
|
|
|
449
438
|
```ts
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
439
|
+
type TabDef = { label: string, icon?: { svg: string, tintColor: string | null } }
|
|
440
|
+
|
|
441
|
+
const ACTIVE = "var(--primaryColor, #5b8cff)"
|
|
442
|
+
const INACTIVE = "var(--mutedColor, #8a919e)"
|
|
443
|
+
const BAR_BG = "var(--tabbarBg, #15171c)"
|
|
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
|
+
}
|
|
455
466
|
```
|
|
456
467
|
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
(
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
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.
|
|
468
|
+
Keep the `defineTabs({...})` declaration and `.screen("<tab>", ...)` / `.bar("<tab>")` call shapes
|
|
469
|
+
intact — the board parses them.
|
|
470
|
+
|
|
471
|
+
## Icons & fonts — vendored only
|
|
472
|
+
|
|
473
|
+
`assetIcon("pack:name")` and `font("id")` are compile-time macros resolved from files ALREADY in
|
|
474
|
+
the project: icons from `design/assets/icons/<pack>/<name>.svg`, font faces from
|
|
475
|
+
`design/assets/fonts/` (both appear under `[Assets]`). There is NO registry access at compile time
|
|
476
|
+
here — an id that isn't vendored is a compile error.
|
|
477
|
+
|
|
478
|
+
- Vendored icons listed in `[Assets]` → use them freely; wrap each in a tiny factory in `shared/ui.ts`
|
|
479
|
+
(`Icon.bell = (c, s = 20) => UIImage(assetIcon("lucide:bell")).style({ width: s, height: s, tintColor: c })`).
|
|
480
|
+
- Not vendored → text glyphs (`✓ ✕ + ← ★ ♥ ⚙`) or simple shapes (a `UIBox`-style circle/bar), and
|
|
481
|
+
stay on the default font. Mention once that running `lecodes design` locally can vendor real
|
|
482
|
+
icons/fonts.
|
|
479
483
|
|
|
480
484
|
## spec.md — the concept
|
|
481
485
|
|