lecodes-design
v0.10.0
Published
LeCodes Design — a live canvas of app screens for AI-driven prototyping. Dev server + browser board, launched via `lecodes design`.
Readme
lecodes-design
LeCodes Design — a live canvas of app screens for AI-driven prototyping. An intent-capture
layer: each screen on the canvas is an executable LeCodes render (real code on mock data), the
edges between them are drawn navigation intent, and the whole folder is the spec an AI builds the
real app from. See lecodes-design-concept.md at the repo root for the full concept.
Launched through the CLI — this package is the dev server + the browser board. It works inside a cloned lecodes project or standalone in any folder (no login or server project needed):
npm install -g lecodes-design # alongside lecodes-cli — install it the same way you installed the CLI
lecodes design init # scaffold design/ (screens/, shared/, meta.json, CLAUDE.md)
lecodes design # start the dev server + open the canvas
lecodes design snapshot # headless JSON/PNG of every screen (Claude's eyes)How it works
design/
screens/login.ts ← one file = one screen; default-exports UIScreen([...]); islands
shared/tokens.ts ← design tokens + reusable components (only allowed import)
meta.json ← the human-owned graph: positions, descriptions, edges
CLAUDE.md ← the conventions, written for the AI- The dev server (
src/server, zero runtime deps) watches the design folder, compiles each screen on demand through the CLI's pipeline (chisel + the vendored SDK — the CLI injectscompileScreen), serves the project files forasset()urls, and pushes SSE change events. - The board (
src/app, Vue 3) renders every screen live via lecodes-renderer (canvas-ui): each tile is its own creator-ui WASM instance +<canvas>, executing the compiled bundle with_creatorUIbound to it — the browser twin of the renderer's headless entry. Pan/zoom the board (wheel / two-finger scroll = pan, ctrl+wheel / pinch = zoom, middle-button drag = pan from anywhere — even over a screen, double-click = fit). Chrome lives where its effect happens: the mode bar (bottom center, lucide icons) holds the four cursor modes; the view pill (bottom right) holds device shell (popover: None/iOS/Android), hints, −/%/+/fit; the dock-panel buttons (Concept 💡, Comments) render in the host's top bar through the exportedBoardToolbar; and Arrange is a quiet icon button hung beside the mode bar (out of flow — the bar never shifts), always reachable, that wears an accent count badge — how many screens the re-layout would move — when pressing it would genuinely tidy:arrangeScreensruns client-side (no endpoint), and the offer appears only when the flow structure changes (screens/edges/ groups/tabs — never in reaction to hand-moving a tile) and the re-layout wins by a clear score margin; while up it re-checks on every change and retires the moment it stops helping. The dot grid lives in world space; edges draw as labeled arrows. - Tiles follow a visible-modes model (Figma grammar, Esc always lands on Select):
Select (
V) — click selects a tile (chrome ring, role editor, mentions), drag anywhere on it moves it (release writesposintometa.json), arrow keys nudge (Shift = ×10), double-click dives into Interact. Drag on empty canvas sweeps a marquee: every screen it touches joins the selection (Shift adds, Shift+click toggles one tile, Ctrl/Cmd+A selects all), the whole selection moves and nudges as one, and 2+ screens raise a selection bar above the sweep — count · Group (auto-named, the chip opens in rename) · Ungroup · Arrange (tidies ONLY the selection, anchored where it is — the rest of the board never moves). Clicking a group's label chip selects its members, so the same bar arranges a group in place; Hand (H) — everything pans (with the Select tool, panning is the middle button and the wheel/touchpad), tiles are inert; Interact (P) — every tile is a working prototype without selecting: the body forwards pointer input (buttons, scroll, router, drags), and focusing an in-screen input shows the simulated keyboard band + captures real keystrokes; Comment (C, aliasM) — every click on a screen pins a comment. Read-only hosts (the share page) open in Interact; hints moved toI, the Concept panel to⇧C. The header ↻ restarts a tile's prototype state. - The flow graph is edited on the canvas too: drag the connect handle (right edge of a tile,
on hover) onto another screen to draw an edge; click an edge (or its label) to edit its
label/activator or delete it — the activator field offers a dropdown of the source screen's
live named elements; click a tile's header note to edit the screen description.
Everything writes back into
meta.json— the file Claude reads as the app map. Edges between the same pair of screens fan out instead of overlapping. - Auto-arrange (the Arrange chip, or
lecodes design arrange) re-lays-out the flow graph as columns reading left→right (a small deterministic Sugiyama layout), so edges stop crossing and diving under tiles. It only writes when the result beats the current layout — a hand-arranged board is left alone — tiles glide into place, and Ctrl+Z undoes it like any other board edit: the board keeps a global Undo/Redo history (Ctrl+Z / Ctrl+Shift+Z or Ctrl+Y) over everything it authors — moves, nudges, group/ungroup/rename/dissolve, arranges (full and scoped), edge add/edit/delete, description and role edits. Entries are field-scoped inverse patches, so Claude's concurrent meta.json edits survive an undo untouched. A screen with noposis already auto-placed next to its flow neighbours on the board (computed live inreadDesignState, never written) — so the AI adds screens with edges but no coordinates and they land sensibly.lecodes design arrange --newjust commits those positions to meta.json (placing only the position-less screens, never moving the rest). Seeauto-arrange.md. - Tab bars are a surface, not edges (
tabbar-surface.md): declare the bar once inshared/tabs.ts(export const mainTabs = defineTabs({ home: {…}, news: {…} })— keys are the root screen ids) and mount it per screen (mainTabs.screen("news", […])). The board discovers it from source (like states) and draws a rail through the tab roots + a faint lane band per tab — zero edges for tab switching; bar-implied edges in meta.json are flagged for deletion, and arrange stacks each hub's lanes in tab order (rows are tabs, columns are depth). Same-screen edges (home → home@empty) render as amber state chips on their activator element — clicking one switches the tile's state; tapping a tab button in a live prototype focuses that tab's root. - Groups carve the board into named sections (Onboarding, Checkout…). Membership lives on the
screen (
"group"in its meta entry — the AI sets it as it creates screens); each group draws as a tinted region with a label chip you can drag to move the whole group, ✎ to rename, ✕ to dissolve, and click to focus (dimming the rest). A selected tile's header gets a chip to change its group. Arrange treats each group as a sub-flow and lays the groups out left→right by their cross-group edges. Seegroups.md. - Text is edited on the canvas: double-click any rendered text to edit it in place — the
edit rewrites the string literal in the screen's source file (Enter commits, Escape
cancels, Shift+Enter inserts a newline). This works through the SDK's creation-site capture +
the bundle's source map, so it lands exactly where
UIText(...)was called — including throughshared/component wrappers. Only literal strings are editable: text computed at runtime (interpolation,node.text = …) is declined with a notice instead of guessed at. - Images are replaced on the canvas: double-click an image to pick a file, or drag an
image file from your OS onto it (the target highlights). The upload is stored under
design/assets/(names deduped; byte-identical files reused) and the image's source reference is rewritten — an inlineasset("…")argument, an asset held in a variable (const man = asset("…")), a hand-writtenimport man from "../assets/man.png", or an asset exported from ashared/file; a remote-URL literal is converted to a localasset()reference. The tile previews the new image instantly; dynamic or ambiguous sources are declined safely. - Device shell (the shell popover in the view pill) frames every tile in
that device's chrome — iOS Dynamic Island + home indicator + iPhone-14-Pro corner rounding,
Android status bar + 3-button navigation bar — and resolves the
safe-*/comfort-*style keywords to that device's real insets (the same values aslecodes render --device), including the edge kind: Android's status/nav bars are declared exact-height bars, socomfort-*adds its knob past them (apb: "comfort-bottom"tab bar keeps a gap above the button bar), while iOS insets are clearance zones wherecomfort-*floors — exactly what each real device computes. Switching rebuilds each prototype from scratch under the new insets (compiles are server-cached, so it's one bundle re-run per tile), so everything recalculates — including screens parked in a router stack. A screen that skipssafe-topvisibly collides with the island/status bar, and Android's translucent nav bar shows content bleeding under the back/home/recents buttons — safe-area misuse is caught at a glance. "None" in the shell popover restores the abstract screen (orientation-default insets). A view preference, remembered locally — never written tometa.json. - Comments (
design-comments-plan.md) are Figma-style pins. The Comment mode (C/Mor the mode bar) makes every click on a screen drop a pin and open the composer — the tool stays armed for a run of comments, Esc or another mode leaves it, and a first-visit nudge above the bar points shared-board reviewers at it. Comments and Concept open from the host's top bar viaBoardToolbar(one component, so the three hosts stop hand-rolling their own buttons); the two share one left dock (never both at once); on a narrow viewport it overlays the board instead of docking. The thread window is anchored to its pin in world space, so panning and zooming carry it along, and opening a thread from the list focuses its screen and opens it on the pin. Your own pins drag: pick one up and drop it anywhere, including on another screen, and it re-anchors to whatever named element is under the drop. A pin anchors to the screen + the state the tile was showing + the nearest named element under the cursor, with a normalized point as fallback, so it survives a re-layout and reads as "thesubmitbutton oncheckout@empty". Pins for another state collapse into a header chip rather than drawing over a layout they weren't written against; a thread whose screen is gone lands in the panel's Detached section instead of vanishing.?c=7opens thread #7 directly. On the platform the threads live in the database — not in project files — so a reviewer holding the public share link can write one without an account and without producing a commit; the developer sees it live in the editor's Design tab. Anonymous authors get a display name plus a browser key that grants only "edit/delete my own"; the project owner can switch commenting off without hiding the conversation that already happened. Locally (lecodes design, including standalone folders with no project) the threads live indesign/comments/— one file per thread, attributed viagit config user.name— and Claude reads them through thedesign_commentsMCP tool: open threads are its task list, closed withresolve_commentonce the change is actually made, answered withcomment_reply, opened withcomment_add. On a cloned project the running server mirrors the platform's open threads in; everything written locally queues untillecodes pushdelivers it after the design it describes.lecodes design comments [--all]prints the same list in the terminal, grouped by screen, andcomments add / reply <n> / resolve <n>write the same store with no server running (--aisigns the write as Claude — attribution the reviewer is owed). - Hint mode (the scan-eye toggle in the view pill, or
I) outlines what the canvas can do something with, on every tile: interactive elements in blue, in-place-editable texts in green, replaceable images in purple (a legend appears next to the view pill). - Concept panel (the 💡 button in the host's top bar, or
⇧C) docks a read-only reader ofspec.md— the narrative half of the design (data model, actors, rules & decisions) — beside the screens, so the developer can review the decisions a backend gets generated from without leaving the board. It renders the markdown (headings, tables, lists, code) with a table of contents; every[[screen-id]]link becomes a chip that focuses that screen on the board ([[id@state]]also switches the tile's state), and a broken[[link]]is flagged (with a count on the 💡 button). The reverse direction: a selected screen shows an "in concept · N" chip that opens a compact popover of the exactspec.mdlines mentioning it (the per-screen "why does this exist / what rules touch it" view) — each line jumps the panel to that exact mention, and a footer opens the panel to cycle them. When a design has nospec.md, the panel offers a one-click Create spec.md (same scaffold aslecodes design init). Editing stays in the file — the panel is read-first. A companion toggle (⚑) shows a non-blocking dot on screens with no role tagged. Both are off-by-nothing: everything is toggleable. - Edges and their activators cross-highlight: selecting or hovering an edge lights up the element its activator names on the source screen (amber pulsing ring + ⚡ tag — dropdown options preview live while the editor is open); hovering that element on the tile lights the edge(s) up in return, so the flow graph reads from either end.
- Screens hot-reload: edit
screens/x.ts→ only that tile recompiles and re-runs; editshared/→ all tiles reload; editmeta.json→ positions/edges update; editspec.md→ only the concept panel refreshes (no screen recompiles).
Because the same render tree drives the board, lecodes design snapshot (JSON/PNG, via
lecodes-renderer headless), and the future app — what Claude "sees" is what you see. The same
screens are also reachable from the render CLI — lecodes render <id|path> [--state <s>] compiles
one screen file the same way and runs it through the full lecodes render flag set (--clip,
--device, --script, --desktop), at the screen's board frame.
Development (of this package)
bun run build # vite build (dist/app) + server bundle (dist/server.js)
bun run typecheck # vue-tsc
bun run dev # vite dev server for the board, proxying to a running design serversrc/app/wasm/ holds a copy of creator-ui's emscripten glue + wasm (same pattern as the viewer);
re-copy from lecodes-renderer/src/layout/creatorUi/ when the layout engine is rebuilt.
