lecodes-cli 0.13.1 → 0.14.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/index.js +182 -89
- package/package.json +3 -3
- package/runtime/sdk/compile/assetIconMacro.ts +6 -4
- package/runtime/sdk/inject.ts +1 -0
- package/runtime/sdk/ui/UI.ts +1 -1
- package/runtime/sdk/ui/UITabs.ts +65 -4
- package/runtime/sdk-types.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1826,7 +1826,7 @@ var ICON_ID, isIdentStart2 = (c2) => c2 >= "A" && c2 <= "Z" || c2 >= "a" && c2 <
|
|
|
1826
1826
|
return new Error(`assetIcon: unknown icon pack "${pack}" (in "${pack}:${name}"). Available packs: ${packs.join(", ")}.`);
|
|
1827
1827
|
} catch {}
|
|
1828
1828
|
}
|
|
1829
|
-
return new Error(`assetIcon: can't reach the icon registry${registry ? ` (${registry})` : ""} to resolve "${pack}:${name}". ` + `Check your connection, or set LECODES_ICON_REGISTRY.`);
|
|
1829
|
+
return Object.assign(new Error(`assetIcon: can't reach the icon registry${registry ? ` (${registry})` : ""} to resolve "${pack}:${name}". ` + `Check your connection, or set LECODES_ICON_REGISTRY.`), { transient: true });
|
|
1830
1830
|
}, VENDORED_ICON_PATH, collectVendoredIcons = (entries) => {
|
|
1831
1831
|
const map = new Map;
|
|
1832
1832
|
for (const e of entries) {
|
|
@@ -43098,6 +43098,13 @@ var SPEC_MD, META_JSON = `{
|
|
|
43098
43098
|
},
|
|
43099
43099
|
"edges": []
|
|
43100
43100
|
}
|
|
43101
|
+
`, META_JSON_DESKTOP = `{
|
|
43102
|
+
"defaultSize": [1280, 800],
|
|
43103
|
+
"screens": {
|
|
43104
|
+
"home": { "pos": [0, 0], "desc": "Example screen — replace me", "role": "list" }
|
|
43105
|
+
},
|
|
43106
|
+
"edges": []
|
|
43107
|
+
}
|
|
43101
43108
|
`, TOKENS_TS = `// Design tokens shared by every screen. Colors are hex strings (the engine parses hex only).
|
|
43102
43109
|
// App font: one theme() call here sets the default text font everywhere. font("id") installs a
|
|
43103
43110
|
// registry family (vendored at compile time) and returns its name — no loading code needed.
|
|
@@ -43114,56 +43121,30 @@ export const colors = {
|
|
|
43114
43121
|
export const spacing = { xs: 4, sm: 8, md: 16, lg: 24 }
|
|
43115
43122
|
|
|
43116
43123
|
export const radius = { sm: 8, md: 12, lg: 20 }
|
|
43117
|
-
`, TABS_TS = `// The app's tab bar
|
|
43124
|
+
`, TABS_TS = `// The app's tab bar — declare it here ONCE; the design canvas discovers it from this file:
|
|
43118
43125
|
//
|
|
43119
43126
|
// export const mainTabs = defineTabs({
|
|
43120
43127
|
// home: { label: "Home", icon: assetIcon("lucide:house") },
|
|
43121
43128
|
// news: { label: "News", icon: assetIcon("lucide:newspaper") },
|
|
43122
43129
|
// })
|
|
43123
43130
|
//
|
|
43124
|
-
//
|
|
43131
|
+
// \`defineTabs\` is an SDK global — the standard themed bar, the same look the built app's UITabs
|
|
43132
|
+
// renders. The KEYS are screen ids — each tab's root screen (screens/<key>.ts), in tab order,
|
|
43133
|
+
// literals only. Icons are always \`assetIcon("pack:name")\` calls — never hand-written
|
|
43134
|
+
// \`{ svg }\` objects (those silently render blank).
|
|
43135
|
+
//
|
|
43125
43136
|
// Every screen that shows the bar mounts it with a literal tab id — tab roots AND pushed details
|
|
43126
43137
|
// that keep the bar:
|
|
43127
43138
|
//
|
|
43128
43139
|
// export default () => mainTabs.screen("news", [ ...content ])
|
|
43129
43140
|
//
|
|
43130
|
-
//
|
|
43131
|
-
//
|
|
43132
|
-
//
|
|
43133
|
-
//
|
|
43134
|
-
|
|
43135
|
-
|
|
43136
|
-
|
|
43137
|
-
// Theme-variable driven — theme({ primaryColor, mutedColor, tabbarBg, screenBg }) anywhere
|
|
43138
|
-
// restyles every screen's bar at once; the fallbacks keep an untouched scaffold looking right.
|
|
43139
|
-
const ACTIVE = "var(--primaryColor, #5b8cff)"
|
|
43140
|
-
const INACTIVE = "var(--mutedColor, #8a919e)"
|
|
43141
|
-
const BAR_BG = "var(--tabbarBg, #15171c)"
|
|
43142
|
-
const SCREEN_BG = "var(--screenBg, #101114)"
|
|
43143
|
-
|
|
43144
|
-
export const defineTabs = <T extends Record<string, TabDef>>(tabs: T) => {
|
|
43145
|
-
const ids = Object.keys(tabs) as (keyof T & string)[]
|
|
43146
|
-
|
|
43147
|
-
/** The bar alone — mount it yourself when the screen owns its whole layout. */
|
|
43148
|
-
const bar = (active: keyof T & string) =>
|
|
43149
|
-
UIRow(ids.map((id) => {
|
|
43150
|
-
const color = id === active ? ACTIVE : INACTIVE
|
|
43151
|
-
return UIButton([
|
|
43152
|
-
...(tabs[id].icon ? [UIImage(tabs[id].icon!).style({ width: 22, height: 22, tintColor: color })] : []),
|
|
43153
|
-
UIText(tabs[id].label).style({ fontSize: 10, color }),
|
|
43154
|
-
]).style({ name: \`tab-\${id}\`, flexDirection: "column", gap: 3, flexGrow: 1, flexBasis: 0, pt: 8, pb: 6 })
|
|
43155
|
-
})).style({ bgColor: BAR_BG, pb: "safe-bottom" })
|
|
43156
|
-
|
|
43157
|
-
/** A screen that sits ON the bar (the common case): [ content, bar ] — the same column the real
|
|
43158
|
-
* app composes as [ UIPager, bar ]. */
|
|
43159
|
-
const screen = (active: keyof T & string, children: UINodeChild[]) =>
|
|
43160
|
-
UIScreen([
|
|
43161
|
-
UIColumn(children).style({ flexGrow: 1, p: 16, pt: "safe-top" }),
|
|
43162
|
-
bar(active),
|
|
43163
|
-
]).style({ bgColor: SCREEN_BG })
|
|
43164
|
-
|
|
43165
|
-
return { ids, bar, screen }
|
|
43166
|
-
}
|
|
43141
|
+
// …or \`mainTabs.bar("news")\` when the screen owns its whole layout. A screen that doesn't mount
|
|
43142
|
+
// it is a full-screen push (bar hidden). Never draw meta.json edges between tab screens — the bar
|
|
43143
|
+
// already navigates; the board renders it as a rail + lanes.
|
|
43144
|
+
//
|
|
43145
|
+
// Restyle via the theme: theme({ primaryColor, mutedColor, tabbarBg, screenBg }) in shared/tokens.ts
|
|
43146
|
+
// recolors every screen's bar (and the built app's UITabs) at once. Keep the defineTabs({...})
|
|
43147
|
+
// declaration and the .screen("<tab>", …) / .bar("<tab>") call shapes intact — the canvas parses them.
|
|
43167
43148
|
`, HOME_SCREEN_TS = `import { colors, spacing, radius } from "../shared/tokens"
|
|
43168
43149
|
|
|
43169
43150
|
// Mock data: the domain shown on realistic values. Inline, per screen.
|
|
@@ -43217,13 +43198,13 @@ This is an intent-capture stage — screens run on inline mock data, before any
|
|
|
43217
43198
|
## Read this first, then meta.json
|
|
43218
43199
|
|
|
43219
43200
|
\`meta.json\` is the app map: every screen (position, description) and the edges between them
|
|
43220
|
-
(navigation intent). **Read it before opening any screen file.**
|
|
43221
|
-
|
|
43222
|
-
|
|
43223
|
-
|
|
43224
|
-
\`lecodes design arrange
|
|
43225
|
-
|
|
43226
|
-
positions
|
|
43201
|
+
(navigation intent). **Read it before opening any screen file.** When you create a screen, add its
|
|
43202
|
+
meta entry **without a \`pos\`** and add edges — the board places it from its structure (its tab
|
|
43203
|
+
lane, its group region, its flow neighbours) and **persists that position itself**; you never
|
|
43204
|
+
hand-pick coordinates (you can't see the board). To tidy after a batch of changes, call the
|
|
43205
|
+
\`arrange_screens\` MCP tool (or \`lecodes design arrange\`) — the layout engine knows tab rails,
|
|
43206
|
+
group regions and flow direction. Never reposition or rewrite existing \`pos\` values or
|
|
43207
|
+
human-authored descriptions — positions the developer dragged are theirs.
|
|
43227
43208
|
|
|
43228
43209
|
\`\`\`json
|
|
43229
43210
|
{
|
|
@@ -43247,11 +43228,15 @@ positions or descriptions.
|
|
|
43247
43228
|
- **An edge from a screen to itself** (\`"from": "home", "to": "home@empty"\`) is a *state link*: the
|
|
43248
43229
|
board draws it as a chip on the activator element (clicking it switches the tile's state), never
|
|
43249
43230
|
as an arrow. Use it to record "tapping X puts this screen into state Y".
|
|
43250
|
-
- **Group
|
|
43251
|
-
|
|
43252
|
-
|
|
43253
|
-
|
|
43254
|
-
|
|
43231
|
+
- **Group only what the tab bar doesn't already section.** Screens on a tab bar are sectioned by
|
|
43232
|
+
their lanes — never set a \`group\` on a tab root or any screen that mounts the bar (a group that
|
|
43233
|
+
duplicates a lane draws nothing and gets flagged for deletion). Group the screens *outside* the
|
|
43234
|
+
bar by user-facing area — \`"group": "Onboarding"\` — 2–6 screens per group (Onboarding,
|
|
43235
|
+
Recovery, Pickers…; on an app with no tab bar that means every screen). Groups render as labeled
|
|
43236
|
+
regions and \`arrange\` lays each out as its own sub-flow, so grouping keeps big boards legible.
|
|
43237
|
+
Semantic clustering is your job (you're good at it); coordinates are \`arrange\`'s (it can see
|
|
43238
|
+
the board, you can't). Don't regroup existing screens unless asked — groups are part of the
|
|
43239
|
+
human's map.
|
|
43255
43240
|
- **Tag a screen's \`role\`** in its meta entry — its *kind* (\`list\`, \`detail\`, \`form\`, \`auth\`,
|
|
43256
43241
|
\`modal\`…). Optional, renders on the board, captures concept the screen source can't;
|
|
43257
43242
|
\`lecodes design check\` lists screens still untagged (never blocking). *(A screen's **states** are
|
|
@@ -43322,9 +43307,11 @@ keep in the chat.
|
|
|
43322
43307
|
|
|
43323
43308
|
Most apps have one — declare it ONCE, in source, and never draw edges for it:
|
|
43324
43309
|
|
|
43325
|
-
- \`
|
|
43310
|
+
- \`defineTabs\` is an SDK global (the standard themed bar — the same look the built app's
|
|
43311
|
+
\`UITabs\` renders). Declare the bar in \`shared/tabs.ts\`:
|
|
43326
43312
|
\`export const mainTabs = defineTabs({ home: { label: "Home", icon: assetIcon("lucide:house") }, … })\`
|
|
43327
|
-
— the **keys are screen ids** (each tab's root screen, in tab order), literals only
|
|
43313
|
+
— the **keys are screen ids** (each tab's root screen, in tab order), literals only, and icons
|
|
43314
|
+
are always \`assetIcon(…)\` calls.
|
|
43328
43315
|
- Every screen that shows the bar mounts it with a **literal** tab id:
|
|
43329
43316
|
\`export default () => mainTabs.screen("news", [ …content ])\` — tab roots AND pushed detail
|
|
43330
43317
|
screens that keep the bar (the news lane's article screen mounts \`"news"\` too). A screen that
|
|
@@ -43332,10 +43319,13 @@ Most apps have one — declare it ONCE, in source, and never draw edges for it:
|
|
|
43332
43319
|
- The canvas discovers all of this from the source (like screen states) and draws the bar as a
|
|
43333
43320
|
**rail + lane bands** — tab switching needs NO edges, and \`design_state\` serves the discovered
|
|
43334
43321
|
\`hubs\`. In the live prototype, tapping a tab button jumps the board to that tab's root screen.
|
|
43322
|
+
- The lane bands replace groups for bar screens: don't \`group\` anything that mounts the bar
|
|
43323
|
+
(see the meta.json section) — groups are for the flows *outside* the bar.
|
|
43335
43324
|
- Style through theme variables: the bar reads \`var(--primaryColor)\`, \`var(--mutedColor)\`,
|
|
43336
43325
|
\`var(--tabbarBg)\`, \`var(--screenBg)\` (with fallbacks) — one \`theme({ primaryColor: … })\` call
|
|
43337
|
-
restyles it app-wide
|
|
43338
|
-
declaration and \`.screen("<tab>", …)\` call shapes intact (the canvas
|
|
43326
|
+
in \`shared/tokens.ts\` restyles it app-wide (and the built app's bar with it). Keep the
|
|
43327
|
+
\`defineTabs({ … })\` declaration and \`.screen("<tab>", …)\` call shapes intact (the canvas
|
|
43328
|
+
parses them).
|
|
43339
43329
|
|
|
43340
43330
|
## Screen states (variants)
|
|
43341
43331
|
|
|
@@ -43497,8 +43487,8 @@ JSON by hand (a message missing its \`pending\` flag is never delivered, and not
|
|
|
43497
43487
|
1. \`board_activity\` (if the server is up), then \`design_comments\` for anything open, then read
|
|
43498
43488
|
\`meta.json\`, the \`shared/\` signatures, and only the screens you need.
|
|
43499
43489
|
2. Create/edit screen files; register new screens in \`meta.json\` **without \`pos\`** and add edges —
|
|
43500
|
-
|
|
43501
|
-
|
|
43490
|
+
the board places them from their lane/group/flow structure and persists the result. After a
|
|
43491
|
+
larger batch, \`arrange_screens\` (MCP) or \`lecodes design arrange\` re-tidies the whole board.
|
|
43502
43492
|
3. \`check_screens\` → \`render_screen\` (or \`lecodes design snapshot\`) → verify → fix.
|
|
43503
43493
|
4. When the developer edits a description, text, or the flow on the canvas — visible in
|
|
43504
43494
|
\`board_activity\` and in \`meta.json\`/the screen source — treat it as the updated spec.
|
|
@@ -43600,12 +43590,17 @@ var DEFAULT_DIR = "design", DEFAULT_PORT = 4477, getContext = (args) => {
|
|
|
43600
43590
|
if (!existsSync19(dir))
|
|
43601
43591
|
return [];
|
|
43602
43592
|
return readdirSync10(dir).filter((f3) => f3.endsWith(".ts") && !f3.startsWith(".")).map((f3) => f3.slice(0, -3)).sort();
|
|
43603
|
-
},
|
|
43593
|
+
}, PHONE_SIZE, readMetaFrames = (designDir) => {
|
|
43604
43594
|
try {
|
|
43605
43595
|
const meta = JSON.parse(readFileSync18(join19(designDir, "meta.json"), "utf8"));
|
|
43606
|
-
|
|
43596
|
+
const d = meta?.defaultSize;
|
|
43597
|
+
const valid = Array.isArray(d) && d.length === 2 && d.every((n2) => typeof n2 === "number" && Number.isFinite(n2) && n2 >= 64 && n2 <= 8192);
|
|
43598
|
+
return {
|
|
43599
|
+
screens: meta?.screens ?? {},
|
|
43600
|
+
defaultSize: valid ? [Math.round(d[0]), Math.round(d[1])] : PHONE_SIZE
|
|
43601
|
+
};
|
|
43607
43602
|
} catch {
|
|
43608
|
-
return {};
|
|
43603
|
+
return { screens: {}, defaultSize: PHONE_SIZE };
|
|
43609
43604
|
}
|
|
43610
43605
|
}, writeMcpConfig = (ctx, port) => {
|
|
43611
43606
|
const path2 = join19(ctx.root, ".mcp.json");
|
|
@@ -43631,7 +43626,7 @@ var DEFAULT_DIR = "design", DEFAULT_PORT = 4477, getContext = (args) => {
|
|
|
43631
43626
|
log(`${c.green(existing ? "updated" : "created")} .mcp.json (lecodes-design MCP → ${entry.url})`);
|
|
43632
43627
|
}, init = async (ctx, args) => {
|
|
43633
43628
|
const files = [
|
|
43634
|
-
["meta.json", META_JSON],
|
|
43629
|
+
["meta.json", flagBool(args, "desktop") ? META_JSON_DESKTOP : META_JSON],
|
|
43635
43630
|
["spec.md", SPEC_MD],
|
|
43636
43631
|
["README.md", README_HINT],
|
|
43637
43632
|
["CLAUDE.md", CLAUDE_MD],
|
|
@@ -43717,14 +43712,14 @@ var DEFAULT_DIR = "design", DEFAULT_PORT = 4477, getContext = (args) => {
|
|
|
43717
43712
|
}, 120000);
|
|
43718
43713
|
killer.unref?.();
|
|
43719
43714
|
const png2 = flagBool(args, "png") || flagStr(args, "png") !== undefined;
|
|
43720
|
-
const
|
|
43715
|
+
const frames = readMetaFrames(ctx.designDir);
|
|
43721
43716
|
const outDir = flagStr(args, "out-dir") ?? join19(ctx.designDir, ".snapshots");
|
|
43722
43717
|
const single = targets.length === 1 && requested.length === 1 && !png2 && !allStates && !flagStr(args, "out-dir");
|
|
43723
43718
|
const onConsole = flagBool(args, "logs") ? (level, text) => logErr(`${c.dim(`[${level}]`)} ${text}`) : undefined;
|
|
43724
43719
|
if (!single)
|
|
43725
43720
|
mkdirSync14(outDir, { recursive: true });
|
|
43726
43721
|
for (const id of targets) {
|
|
43727
|
-
const [width, height] =
|
|
43722
|
+
const [width, height] = frames.screens[id]?.size ?? frames.defaultSize;
|
|
43728
43723
|
let plan = [{ state: undefined, bases: [id] }];
|
|
43729
43724
|
if (discover) {
|
|
43730
43725
|
const { states, canonical } = discover(readFileSync18(join19(ctx.designDir, "screens", `${id}.ts`), "utf8"));
|
|
@@ -43798,7 +43793,8 @@ var DEFAULT_DIR = "design", DEFAULT_PORT = 4477, getContext = (args) => {
|
|
|
43798
43793
|
state,
|
|
43799
43794
|
assets: "disk"
|
|
43800
43795
|
});
|
|
43801
|
-
const
|
|
43796
|
+
const frames = readMetaFrames(ctx.designDir);
|
|
43797
|
+
const [width, height] = frames.screens[id]?.size ?? frames.defaultSize;
|
|
43802
43798
|
const warnings = [];
|
|
43803
43799
|
let json;
|
|
43804
43800
|
let png2;
|
|
@@ -43849,11 +43845,12 @@ var DEFAULT_DIR = "design", DEFAULT_PORT = 4477, getContext = (args) => {
|
|
|
43849
43845
|
const designServer = await loadDesignServer(ctx);
|
|
43850
43846
|
const onlyNew = flagBool(args, "new");
|
|
43851
43847
|
const summary = designServer.arrangeDesign(ctx.designDir, { onlyNew });
|
|
43848
|
+
const score = summary.score ? ` (score ${summary.score.before.toFixed(0)} → ${summary.score.after.toFixed(0)}, lower is better)` : "";
|
|
43852
43849
|
if (!summary.changed) {
|
|
43853
|
-
log(onlyNew ? "No unplaced screens — nothing to arrange." :
|
|
43850
|
+
log(onlyNew ? "No unplaced screens — nothing to arrange." : `Already in its arranged shape — nothing moved${score}.`);
|
|
43854
43851
|
return;
|
|
43855
43852
|
}
|
|
43856
|
-
log(`${c.green("✓")} ${onlyNew ? "Placed" : "Arranged"} ${summary.moved} screen${summary.moved === 1 ? "" : "s"} in ${ctx.dirName}/meta.json.`);
|
|
43853
|
+
log(`${c.green("✓")} ${onlyNew ? "Placed" : "Arranged"} ${summary.moved} screen${summary.moved === 1 ? "" : "s"} in ${ctx.dirName}/meta.json${score}.`);
|
|
43857
43854
|
}, share = async (ctx, args) => {
|
|
43858
43855
|
if (!ctx.manifest) {
|
|
43859
43856
|
throw new CliError("`lecodes design share` needs a cloned project. Run it inside one, and push the design first (lecodes push).");
|
|
@@ -44192,6 +44189,7 @@ var init_design = __esm(() => {
|
|
|
44192
44189
|
init_config();
|
|
44193
44190
|
init_api();
|
|
44194
44191
|
init_types4();
|
|
44192
|
+
PHONE_SIZE = [390, 844];
|
|
44195
44193
|
});
|
|
44196
44194
|
|
|
44197
44195
|
// ../../node_modules/.bun/qrcode-terminal@0.12.0/node_modules/qrcode-terminal/vendor/QRCode/QRMode.js
|
|
@@ -45213,7 +45211,7 @@ var require_main = __commonJS((exports, module) => {
|
|
|
45213
45211
|
// package.json
|
|
45214
45212
|
var package_default = {
|
|
45215
45213
|
name: "lecodes-cli",
|
|
45216
|
-
version: "0.
|
|
45214
|
+
version: "0.14.0",
|
|
45217
45215
|
dependencies: {
|
|
45218
45216
|
"@letary/chisel": "^0.6.0",
|
|
45219
45217
|
jimp: "^1.6.1"
|
|
@@ -45226,8 +45224,8 @@ var package_default = {
|
|
|
45226
45224
|
sdk: "workspace:*"
|
|
45227
45225
|
},
|
|
45228
45226
|
peerDependencies: {
|
|
45229
|
-
"lecodes-design": "^0.
|
|
45230
|
-
"lecodes-renderer": "^0.8.
|
|
45227
|
+
"lecodes-design": "^0.6.1",
|
|
45228
|
+
"lecodes-renderer": "^0.8.2",
|
|
45231
45229
|
"lecodes-3d-editor": "^0.2.0"
|
|
45232
45230
|
},
|
|
45233
45231
|
bin: {
|
|
@@ -46346,6 +46344,17 @@ var FIRST_PARTY_PLUGINS = {
|
|
|
46346
46344
|
geolocation: "letary/lecodes-plugins",
|
|
46347
46345
|
push: "letary/lecodes-plugins"
|
|
46348
46346
|
};
|
|
46347
|
+
var readAppConfigAt = (root) => readJson(join12(root, "app.json"));
|
|
46348
|
+
var DESKTOP_DEFAULT_WINDOW = { width: 1280, height: 720 };
|
|
46349
|
+
var desktopDefaultViewport = (root) => {
|
|
46350
|
+
const cfg = readAppConfigAt(root);
|
|
46351
|
+
if (!cfg?.desktop || cfg.ios || cfg.android)
|
|
46352
|
+
return null;
|
|
46353
|
+
return {
|
|
46354
|
+
width: cfg.desktop.window?.width ?? DESKTOP_DEFAULT_WINDOW.width,
|
|
46355
|
+
height: cfg.desktop.window?.height ?? DESKTOP_DEFAULT_WINDOW.height
|
|
46356
|
+
};
|
|
46357
|
+
};
|
|
46349
46358
|
var readJson = (path) => {
|
|
46350
46359
|
if (!existsSync10(path))
|
|
46351
46360
|
return null;
|
|
@@ -48907,6 +48916,7 @@ var create = async (args) => {
|
|
|
48907
48916
|
|
|
48908
48917
|
// src/commands/desktop.ts
|
|
48909
48918
|
init_util();
|
|
48919
|
+
init_manifest();
|
|
48910
48920
|
|
|
48911
48921
|
// src/desktopRenderer.ts
|
|
48912
48922
|
init_util();
|
|
@@ -49057,6 +49067,20 @@ var updateDesktop = async () => {
|
|
|
49057
49067
|
else
|
|
49058
49068
|
note(`Desktop renderer set to ${latest} (was ${before}).`);
|
|
49059
49069
|
};
|
|
49070
|
+
var windowEnv = (opts) => {
|
|
49071
|
+
const env = {};
|
|
49072
|
+
const width = opts.width ?? (opts.height !== undefined ? 1280 : undefined);
|
|
49073
|
+
const height = opts.height ?? (opts.width !== undefined ? 720 : undefined);
|
|
49074
|
+
if (width && height)
|
|
49075
|
+
env.CREATOR_WINDOW_SIZE = `${width}x${height}`;
|
|
49076
|
+
if (opts.minWidth && opts.minHeight)
|
|
49077
|
+
env.CREATOR_MIN_WINDOW_SIZE = `${opts.minWidth}x${opts.minHeight}`;
|
|
49078
|
+
if (opts.title)
|
|
49079
|
+
env.CREATOR_WINDOW_TITLE = opts.title;
|
|
49080
|
+
if (opts.resizable === false)
|
|
49081
|
+
env.CREATOR_WINDOW_FIXED = "1";
|
|
49082
|
+
return env;
|
|
49083
|
+
};
|
|
49060
49084
|
var stageBundle = (js, resources) => {
|
|
49061
49085
|
const runDir = mkdtempSync(join18(tmpdir(), "lecodes-render-"));
|
|
49062
49086
|
writeFileSync14(join18(runDir, "app.js"), js);
|
|
@@ -49071,9 +49095,7 @@ var runDesktopApp = async (js, resources, opts = {}) => {
|
|
|
49071
49095
|
const exe = await resolveDesktopExe();
|
|
49072
49096
|
const runDir = stageBundle(js, resources);
|
|
49073
49097
|
try {
|
|
49074
|
-
const env = { ...process.env };
|
|
49075
|
-
if (opts.width && opts.height)
|
|
49076
|
-
env.CREATOR_WINDOW_SIZE = `${opts.width}x${opts.height}`;
|
|
49098
|
+
const env = { ...process.env, ...windowEnv(opts) };
|
|
49077
49099
|
await new Promise((done, fail) => {
|
|
49078
49100
|
const child = spawn2(exe, [join18(runDir, "app.js")], { cwd: runDir, env, stdio: ["ignore", "inherit", "inherit"] });
|
|
49079
49101
|
child.on("error", (e2) => fail(new CliError(`Can't run the desktop host: ${e2.message}`)));
|
|
@@ -49088,6 +49110,15 @@ var runDesktopApp = async (js, resources, opts = {}) => {
|
|
|
49088
49110
|
rmSync4(runDir, { recursive: true, force: true });
|
|
49089
49111
|
}
|
|
49090
49112
|
};
|
|
49113
|
+
var runDesktopDev = async (url, opts = {}) => {
|
|
49114
|
+
const exe = await resolveDesktopExe();
|
|
49115
|
+
const env = { ...process.env, ...windowEnv(opts) };
|
|
49116
|
+
await new Promise((done, fail) => {
|
|
49117
|
+
const child = spawn2(exe, [url], { env, stdio: ["ignore", "ignore", "inherit"] });
|
|
49118
|
+
child.on("error", (e2) => fail(new CliError(`Can't run the desktop host: ${e2.message}`)));
|
|
49119
|
+
child.on("exit", () => done());
|
|
49120
|
+
});
|
|
49121
|
+
};
|
|
49091
49122
|
var runDesktopRender = async (js, resources, opts) => {
|
|
49092
49123
|
const exe = await resolveDesktopExe();
|
|
49093
49124
|
const runDir = stageBundle(js, resources);
|
|
@@ -49141,11 +49172,24 @@ ${tail.join("").trimEnd()}` : "";
|
|
|
49141
49172
|
|
|
49142
49173
|
// src/commands/desktop.ts
|
|
49143
49174
|
var run = async (args) => {
|
|
49175
|
+
let root;
|
|
49176
|
+
try {
|
|
49177
|
+
root = findProjectRoot(process.cwd());
|
|
49178
|
+
} catch {
|
|
49179
|
+
root = process.cwd();
|
|
49180
|
+
}
|
|
49181
|
+
const appCfg = readAppConfigAt(root);
|
|
49182
|
+
const win = appCfg?.desktop?.window;
|
|
49144
49183
|
const { js, resources } = await buildBundle(args);
|
|
49145
|
-
const width = Number(flagStr(args, "width")) || undefined;
|
|
49146
|
-
const height = Number(flagStr(args, "height")) || undefined;
|
|
49147
49184
|
note("Launching LeCodes Desktop… (close the window to return)");
|
|
49148
|
-
await runDesktopApp(js, resources, {
|
|
49185
|
+
await runDesktopApp(js, resources, {
|
|
49186
|
+
width: Number(flagStr(args, "width")) || win?.width,
|
|
49187
|
+
height: Number(flagStr(args, "height")) || win?.height,
|
|
49188
|
+
minWidth: win?.minWidth,
|
|
49189
|
+
minHeight: win?.minHeight,
|
|
49190
|
+
resizable: win?.resizable,
|
|
49191
|
+
title: flagStr(args, "title") ?? win?.title ?? appCfg?.name
|
|
49192
|
+
});
|
|
49149
49193
|
};
|
|
49150
49194
|
var status = async () => {
|
|
49151
49195
|
const selected = selectedDesktopVersion();
|
|
@@ -49787,6 +49831,19 @@ ${result.error}`);
|
|
|
49787
49831
|
note(`Token stored in ${LECODES_DIR}/dev.json (delete it to rotate the URL).`);
|
|
49788
49832
|
log("");
|
|
49789
49833
|
log(`Watching ${root} — save a file to hot-reload. Ctrl+C to stop.`);
|
|
49834
|
+
if (flagBool(args, "desktop")) {
|
|
49835
|
+
const appCfg = readAppConfigAt(root);
|
|
49836
|
+
const win = appCfg?.desktop?.window;
|
|
49837
|
+
note("Opening LeCodes Desktop against the dev server…");
|
|
49838
|
+
runDesktopDev(server.url, {
|
|
49839
|
+
width: win?.width,
|
|
49840
|
+
height: win?.height,
|
|
49841
|
+
minWidth: win?.minWidth,
|
|
49842
|
+
minHeight: win?.minHeight,
|
|
49843
|
+
resizable: win?.resizable,
|
|
49844
|
+
title: win?.title ?? appCfg?.name ?? name
|
|
49845
|
+
}).then(() => note("Desktop window closed — the dev server keeps running (Ctrl+C to stop)."), (e2) => warnErr(`Desktop host: ${e2 instanceof Error ? e2.message : String(e2)}`));
|
|
49846
|
+
}
|
|
49790
49847
|
};
|
|
49791
49848
|
|
|
49792
49849
|
// src/commands/diff.ts
|
|
@@ -50030,6 +50087,13 @@ const screen = UIScreen(
|
|
|
50030
50087
|
|
|
50031
50088
|
screen.open()
|
|
50032
50089
|
`;
|
|
50090
|
+
var appJsonDesktop = (name) => JSON.stringify({
|
|
50091
|
+
name,
|
|
50092
|
+
desktop: {
|
|
50093
|
+
window: { width: 1280, height: 800, minWidth: 640, minHeight: 480 }
|
|
50094
|
+
}
|
|
50095
|
+
}, null, 2) + `
|
|
50096
|
+
`;
|
|
50033
50097
|
var readmeTemplate = (name) => `# ${name}
|
|
50034
50098
|
|
|
50035
50099
|
A LeCodes app. The whole SDK is available as globals (\`UIScreen\`, \`UIText\`, \`Scene2D\`, …) —
|
|
@@ -50104,8 +50168,11 @@ var init2 = async (args) => {
|
|
|
50104
50168
|
log(`${c.green("created")} ${rel}`);
|
|
50105
50169
|
created++;
|
|
50106
50170
|
};
|
|
50171
|
+
const forDesktop = flagBool(args, "desktop");
|
|
50107
50172
|
write("main.ts", STARTER2);
|
|
50108
50173
|
write("README.md", readmeTemplate(name));
|
|
50174
|
+
if (forDesktop)
|
|
50175
|
+
write("app.json", appJsonDesktop(name));
|
|
50109
50176
|
write("CLAUDE.md", appClaudeMd(name));
|
|
50110
50177
|
write("tests/counter.flow.json", STARTER_FLOW);
|
|
50111
50178
|
write(".lecodesignore", DEFAULT_IGNORE_FILE);
|
|
@@ -50118,9 +50185,15 @@ var init2 = async (args) => {
|
|
|
50118
50185
|
log("");
|
|
50119
50186
|
success(created ? `Local project ${c.bold(name)} ready${dirArg === "." ? "" : ` in ${c.bold(dirArg)}/`}.` : `Project ${c.bold(name)} already initialized.`);
|
|
50120
50187
|
const cd = dirArg === "." ? "" : `cd ${dirArg} && `;
|
|
50121
|
-
|
|
50122
|
-
|
|
50123
|
-
|
|
50188
|
+
if (forDesktop) {
|
|
50189
|
+
log(` ${c.bold(cd + "lecodes dev --desktop")} run it in a desktop window with hot reload`);
|
|
50190
|
+
log(` ${c.bold(cd + "lecodes test")} run the flow tests headless (desktop-sized)`);
|
|
50191
|
+
log(` ${c.bold(cd + "lecodes desktop run")} open the compiled app in the desktop host`);
|
|
50192
|
+
} else {
|
|
50193
|
+
log(` ${c.bold(cd + "lecodes dev")} run it on a device with hot reload`);
|
|
50194
|
+
log(` ${c.bold(cd + "lecodes test")} run the flow tests headless (no device)`);
|
|
50195
|
+
log(` ${c.bold(cd + "lecodes compile -o app.js")} bundle it for LeCodes Desktop`);
|
|
50196
|
+
}
|
|
50124
50197
|
};
|
|
50125
50198
|
|
|
50126
50199
|
// src/commands/link.ts
|
|
@@ -50470,6 +50543,7 @@ var compileHeadlessBundle = async (args) => {
|
|
|
50470
50543
|
};
|
|
50471
50544
|
|
|
50472
50545
|
// src/commands/render.ts
|
|
50546
|
+
init_manifest();
|
|
50473
50547
|
init_util();
|
|
50474
50548
|
var DEVICE_SAFE_AREA = {
|
|
50475
50549
|
none: { insets: [0, 0, 0, 0], bars: [] },
|
|
@@ -50539,8 +50613,15 @@ var readScenarioFile = (path2) => {
|
|
|
50539
50613
|
return file;
|
|
50540
50614
|
};
|
|
50541
50615
|
var renderDesktop = async (args) => {
|
|
50542
|
-
|
|
50543
|
-
|
|
50616
|
+
let root;
|
|
50617
|
+
try {
|
|
50618
|
+
root = findProjectRoot(process.cwd());
|
|
50619
|
+
} catch {
|
|
50620
|
+
root = process.cwd();
|
|
50621
|
+
}
|
|
50622
|
+
const win = readAppConfigAt(root)?.desktop?.window;
|
|
50623
|
+
const width = Number(flagStr(args, "width")) || win?.width || 390;
|
|
50624
|
+
const height = Number(flagStr(args, "height")) || win?.height || 844;
|
|
50544
50625
|
const frames = Number(flagStr(args, "frames")) || 60;
|
|
50545
50626
|
const timeoutMs = Number(flagStr(args, "timeout")) || 60000;
|
|
50546
50627
|
const pngFlag = flagStr(args, "png") ?? "screenshot.png";
|
|
@@ -50568,10 +50649,11 @@ var render = async (args) => {
|
|
|
50568
50649
|
process.exit(1);
|
|
50569
50650
|
}, timeoutMs);
|
|
50570
50651
|
killer.unref?.();
|
|
50571
|
-
const { js, warnings, useLocalAssets } = await compileHeadlessBundle(args);
|
|
50652
|
+
const { js, warnings, useLocalAssets, root } = await compileHeadlessBundle(args);
|
|
50572
50653
|
const renderer = await requireRenderer();
|
|
50573
|
-
const
|
|
50574
|
-
const
|
|
50654
|
+
const projectViewport = desktopDefaultViewport(root);
|
|
50655
|
+
const width = Number(flagStr(args, "width")) || scenario?.width || projectViewport?.width || 390;
|
|
50656
|
+
const height = Number(flagStr(args, "height")) || scenario?.height || projectViewport?.height || 844;
|
|
50575
50657
|
const settleStr = flagStr(args, "settle") ?? (scenario?.settle !== undefined ? String(scenario.settle) : undefined);
|
|
50576
50658
|
const timeStr = flagStr(args, "time");
|
|
50577
50659
|
const onConsole = flagBool(args, "logs") ? (level, text2) => logErr(`${c.dim(`[${level}]`)} ${text2}`) : undefined;
|
|
@@ -51286,6 +51368,7 @@ var readFlowFile = (abs, displayName) => {
|
|
|
51286
51368
|
var test = async (args) => {
|
|
51287
51369
|
const { js, warnings, useLocalAssets, root } = await compileHeadlessBundle(args);
|
|
51288
51370
|
const renderer = await requireRenderer();
|
|
51371
|
+
const projectViewport = desktopDefaultViewport(root);
|
|
51289
51372
|
const testsDir = resolve12(root, flagStr(args, "dir") ?? "tests");
|
|
51290
51373
|
const designMetaPath = resolve12(root, flagStr(args, "design-dir") ?? "design", "meta.json");
|
|
51291
51374
|
const asJson = flagBool(args, "json");
|
|
@@ -51356,8 +51439,8 @@ var test = async (args) => {
|
|
|
51356
51439
|
try {
|
|
51357
51440
|
const steps = renderer.parseScenarioSteps(kase.steps);
|
|
51358
51441
|
const session = await renderer.createSession(js, {
|
|
51359
|
-
width: Number(flagStr(args, "width")) || kase.width || 390,
|
|
51360
|
-
height: Number(flagStr(args, "height")) || kase.height || 844,
|
|
51442
|
+
width: Number(flagStr(args, "width")) || kase.width || projectViewport?.width || 390,
|
|
51443
|
+
height: Number(flagStr(args, "height")) || kase.height || projectViewport?.height || 844,
|
|
51361
51444
|
settleMs: flagSettle !== undefined ? Number(flagSettle) : kase.settle,
|
|
51362
51445
|
waitNetwork: flagBool(args, "wait-network"),
|
|
51363
51446
|
localAssets: useLocalAssets,
|
|
@@ -51709,7 +51792,11 @@ ${c.bold("Commands:")}
|
|
|
51709
51792
|
init [dir] Scaffold a new LOCAL project (no login/server): a starter
|
|
51710
51793
|
main.ts, a CLAUDE.md + passing flow test (the headless
|
|
51711
51794
|
render/test loop), IDE types + tsconfig, a .lecodesignore.
|
|
51712
|
-
Run it with 'lecodes dev' or 'lecodes compile'
|
|
51795
|
+
Run it with 'lecodes dev' or 'lecodes compile'.
|
|
51796
|
+
--desktop also write an app.json declaring the desktop
|
|
51797
|
+
target (window size; 'dev --desktop',
|
|
51798
|
+
'desktop run' and headless render/test
|
|
51799
|
+
pick it up)${INSTALL_HELP}
|
|
51713
51800
|
login Authorize in the browser and store an access token
|
|
51714
51801
|
--token <pat> store a token from the web UI instead
|
|
51715
51802
|
--api <url> API origin (default https://le.codes)
|
|
@@ -51752,6 +51839,9 @@ ${c.bold("Commands:")}
|
|
|
51752
51839
|
--host <ip> LAN address to advertise (default: auto-detect)
|
|
51753
51840
|
--entry <path> override the entrypoint
|
|
51754
51841
|
--no-qr don't render the QR code
|
|
51842
|
+
--desktop also open the app in the native desktop host
|
|
51843
|
+
(same hot reload; window from app.json's
|
|
51844
|
+
desktop block; closing it keeps the server)
|
|
51755
51845
|
compile Compile the local project to JS (prints to stdout)
|
|
51756
51846
|
(was 'lecodes preview' — the old name still works)
|
|
51757
51847
|
-o, --out <file> write to a file instead of stdout
|
|
@@ -51817,6 +51907,8 @@ ${c.bold("Commands:")}
|
|
|
51817
51907
|
--dir <folder> design folder (default design/)
|
|
51818
51908
|
--no-open don't open the browser
|
|
51819
51909
|
design init Scaffold the design folder (screens/, shared/, meta.json, CLAUDE.md)
|
|
51910
|
+
--desktop desktop-frame board (meta.json defaultSize
|
|
51911
|
+
[1280, 800] instead of the phone frame)
|
|
51820
51912
|
design snapshot [screen…] Render design screens headless (needs 'lecodes-renderer')
|
|
51821
51913
|
one screen → JSON to stdout; else files in design/.snapshots/
|
|
51822
51914
|
--png also rasterize PNGs (needs @napi-rs/canvas)
|
|
@@ -51831,8 +51923,9 @@ ${c.bold("Commands:")}
|
|
|
51831
51923
|
no args shows the version status (selected / downloaded /
|
|
51832
51924
|
newest published)
|
|
51833
51925
|
run compile the current project and open it in a real
|
|
51834
|
-
desktop window (
|
|
51835
|
-
|
|
51926
|
+
desktop window (size/min-size/title default from
|
|
51927
|
+
app.json's desktop block; --width/--height/--title
|
|
51928
|
+
override; --entry overrides the entrypoint)
|
|
51836
51929
|
update move to the newest published release
|
|
51837
51930
|
(cached in ~/.lecodes/renderer)
|
|
51838
51931
|
scene [path] LeCodes Scene Editor — visually edit a .scene.ts (tree, inspector,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lecodes-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.0",
|
|
4
4
|
"dependencies": {
|
|
5
5
|
"@letary/chisel": "^0.6.0",
|
|
6
6
|
"jimp": "^1.6.1"
|
|
@@ -13,8 +13,8 @@
|
|
|
13
13
|
"sdk": "workspace:*"
|
|
14
14
|
},
|
|
15
15
|
"peerDependencies": {
|
|
16
|
-
"lecodes-design": "^0.
|
|
17
|
-
"lecodes-renderer": "^0.8.
|
|
16
|
+
"lecodes-design": "^0.6.1",
|
|
17
|
+
"lecodes-renderer": "^0.8.2",
|
|
18
18
|
"lecodes-3d-editor": "^0.2.0"
|
|
19
19
|
},
|
|
20
20
|
"bin": {
|
|
@@ -16,9 +16,9 @@
|
|
|
16
16
|
*
|
|
17
17
|
* WHERE the SVG bodies come from is the caller's business, passed in as a resolver:
|
|
18
18
|
* - CLI design compile: vendored into the design folder from the icon registry (lock + notices);
|
|
19
|
-
* - backend design board: the already-pushed `design/assets/icons/**` text assets, no registry;
|
|
20
19
|
* - CLI app compiles: the user-level cache (~/.lecodes/cache/icons) + registry on miss;
|
|
21
|
-
* - backend app compile:
|
|
20
|
+
* - backend app compile + design board: vendored `assets/icons/**` text assets first, then a
|
|
21
|
+
* server-side cache + registry on miss.
|
|
22
22
|
* App compiles never write icons into the project — attribution for platform-deployed apps rides
|
|
23
23
|
* the `// icons:` bundle header (buildIconsHeaderValue) and the platform, not an in-repo notice.
|
|
24
24
|
*/
|
|
@@ -358,10 +358,12 @@ export const iconNotFoundError = async (
|
|
|
358
358
|
return new Error(`assetIcon: unknown icon pack "${pack}" (in "${pack}:${name}"). Available packs: ${packs.join(", ")}.`)
|
|
359
359
|
} catch { /* corrupt licenses → fall through to the connectivity error */ }
|
|
360
360
|
}
|
|
361
|
-
|
|
361
|
+
// `transient` lets callers that cache compile results skip caching this one: unlike a missing
|
|
362
|
+
// icon, an unreachable registry is not a property of the source being compiled.
|
|
363
|
+
return Object.assign(new Error(
|
|
362
364
|
`assetIcon: can't reach the icon registry${registry ? ` (${registry})` : ""} to resolve "${pack}:${name}". `
|
|
363
365
|
+ `Check your connection, or set LECODES_ICON_REGISTRY.`,
|
|
364
|
-
)
|
|
366
|
+
), { transient: true })
|
|
365
367
|
}
|
|
366
368
|
|
|
367
369
|
/** `…assets/icons/<pack>/<name>.svg` — an icon the design flow vendored into the project tree. */
|
package/runtime/sdk/inject.ts
CHANGED
package/runtime/sdk/ui/UI.ts
CHANGED
|
@@ -17,7 +17,7 @@ export { UIPopover, type UIPopoverStyle, type UIPopoverAnchor } from './UIPopove
|
|
|
17
17
|
export { UIRow, UIColumn, UIBox, type UIContainerStyle } from './UIContainer'
|
|
18
18
|
export { UIScrollable, type UIScrollableStyle } from './UIScrollable'
|
|
19
19
|
export { UIPager, type UIPagerStyle } from './UIPager'
|
|
20
|
-
export { UITabs, type UITabDef } from './UITabs'
|
|
20
|
+
export { UITabs, type UITabDef, defineTabs, type TabDef, type TabsHandle } from './UITabs'
|
|
21
21
|
|
|
22
22
|
export { UIVirtualizedList } from './UIVirtualizedList'
|
|
23
23
|
|