diff --git a/packages/core/src/design-jsx/render.ts b/packages/core/src/design-jsx/render.ts index a5bd1670f..589d314dc 100644 --- a/packages/core/src/design-jsx/render.ts +++ b/packages/core/src/design-jsx/render.ts @@ -11,22 +11,32 @@ import type { SceneGraph } from '../scene-graph' * Works in both Node/Bun and the browser (no native bindings). */ export function buildComponent(jsxString: string): () => unknown { - const code = ` + const trimmed = jsxString.trim() + + const aliases = ` const __h = React.createElement + const __frag = '' const Frame = 'frame', Text = 'text', Rectangle = 'rectangle', Ellipse = 'ellipse' const Line = 'line', Star = 'star', Polygon = 'polygon', Vector = 'vector' const Group = 'group', Section = 'section', View = 'frame', Rect = 'rectangle' + const Component = 'component', Instance = 'frame' const Icon = 'icon' - return function Component() { return ${jsxString.trim()} } ` - - const result = transform(code, { - transforms: ['typescript', 'jsx'], + const opts = { + transforms: ['typescript', 'jsx'] as Array<'typescript' | 'jsx'>, jsxPragma: '__h', + jsxFragmentPragma: '__frag', production: true - }) + } - return new Function('React', result.code)(React) as () => unknown + let code: string + try { + code = transform(`${aliases}\nreturn function __render() { return ${trimmed} }`, opts).code + } catch { + code = transform(`${aliases}\nreturn function __render() { return <>${trimmed} }`, opts).code + } + + return new Function('React', code)(React) as () => unknown } interface RenderJSXOptions { @@ -43,7 +53,7 @@ export async function renderJSX( graph: SceneGraph, jsxString: string, options?: RenderJSXOptions -): Promise { +): Promise { const Component = buildComponent(jsxString) const element = React.createElement(Component, null) const tree = resolveToTree(element) @@ -52,7 +62,19 @@ export async function renderJSX( throw new Error('JSX must return a Figma element (Frame, Text, etc)') } - return renderTree(graph, tree, options) + if (tree.type === '' && tree.children.length > 0) { + const results: RenderResult[] = [] + for (const child of tree.children) { + if (typeof child === 'string') continue + results.push(await renderTree(graph, child, options)) + } + if (results.length === 0) { + throw new Error('JSX must return a Figma element (Frame, Text, etc)') + } + return results + } + + return [await renderTree(graph, tree, options)] } /** diff --git a/packages/core/src/global.d.ts b/packages/core/src/global.d.ts index 4ba8b5ced..e6d75afed 100644 --- a/packages/core/src/global.d.ts +++ b/packages/core/src/global.d.ts @@ -17,3 +17,8 @@ interface Uint8ArrayConstructor { interface Uint8Array { toBase64(options?: { alphabet?: 'base64' | 'base64url' }): string } + +declare module '*.md' { + const content: string + export default content +} diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 5512b9889..0cdd5a951 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -395,7 +395,8 @@ export { export * from './io' export * from './lint' -export { CODEGEN_PROMPT } from './tools/prompts/codegen-prompt' +export { default as CODEGEN_PROMPT } from './tools/prompts/codegen.md' +export { default as JSX_REFERENCE } from './tools/prompts/jsx-reference.md' export { setPexelsApiKey, setUnsplashAccessKey, diff --git a/packages/core/src/tools/create.ts b/packages/core/src/tools/create.ts index b6b0aad3f..26b62aae8 100644 --- a/packages/core/src/tools/create.ts +++ b/packages/core/src/tools/create.ts @@ -85,11 +85,12 @@ export const render = defineTool({ } } - const result = await renderJSX(figma.graph, args.jsx, { + const results = await renderJSX(figma.graph, args.jsx, { parentId, x: args.x, y: args.y }) + const result = results[0] if (args.replace_id && replaceIndex >= 0) { figma.graph.reorderChild(result.id, parentId, replaceIndex) @@ -98,7 +99,15 @@ export const render = defineTool({ figma.graph.reorderChild(result.id, parentId, args.insert_index) } - return { id: result.id, name: result.name, type: result.type, children: result.childIds } + return { + id: result.id, + name: result.name, + type: result.type, + children: result.childIds, + ...(results.length > 1 + ? { siblings: results.slice(1).map((r) => ({ id: r.id, name: r.name, type: r.type })) } + : {}) + } } }) diff --git a/packages/core/src/tools/prompts/codegen-prompt.ts b/packages/core/src/tools/prompts/codegen.md similarity index 55% rename from packages/core/src/tools/prompts/codegen-prompt.ts rename to packages/core/src/tools/prompts/codegen.md index baddc8ef0..8b91bb6eb 100644 --- a/packages/core/src/tools/prompts/codegen-prompt.ts +++ b/packages/core/src/tools/prompts/codegen.md @@ -1,8 +1,8 @@ -export const CODEGEN_PROMPT = `# Design to Code +# Design to Code You convert Figma designs into production frontend code. You have full access to the design document through tools. Never guess — always read the actual design data. -> Note: Auto-layout and visual property names match the JSX props in the render system. See \`describe\` tool output for semantic role and layout analysis of any node. +> Note: Auto-layout and visual property names match the JSX props in the render system. See `describe` tool output for semantic role and layout analysis of any node. ## Workflow @@ -10,7 +10,7 @@ You convert Figma designs into production frontend code. You have full access to Understand the full picture before writing any code. -\`\`\` +``` get_page_tree → document structure, all top-level frames get_components → reusable components defined by the designer list_variables → design tokens (colors, numbers, strings, booleans) @@ -18,9 +18,10 @@ list_collections → variable collections and modes (light/dark, density, analyze_colors → color palette, frequencies, which colors use variables analyze_typography → font stacks, sizes, weights in use analyze_spacing → gap and padding values, grid compliance -\`\`\` +``` After this step you should know: + - How many screens/pages the design has - What components exist - What the token system looks like (or if there is none) @@ -31,110 +32,114 @@ After this step you should know: Identify the component architecture. -\`\`\` +``` analyze_clusters → find repeated visual patterns that should be components describe (per node) → semantic role, layout direction, visual properties, issues get_jsx (per node) → structural JSX to understand nesting and layout -\`\`\` +``` Build a component map: + - **Screens** — top-level frames that represent pages/views - **Components** — COMPONENT/COMPONENT_SET nodes or repeated patterns from analyze_clusters - **Primitives** — leaf elements (text, icons, dividers) that don't need their own component file For each component determine: + - **Props** — what content varies between instances (text, color, icon, visibility) - **Variants** — if the component has multiple states (default/hover/active, small/medium/large) - **Slots** — where child content is injected ### Step 3 — Extract tokens -\`\`\` +``` list_variables → all variables with values per mode list_collections → collection structure and mode names -\`\`\` +``` Map design variables to code tokens. The approach depends on the target stack: #### Tailwind projects Do NOT create CSS custom properties for font sizes, font weights, spacing, or border radius — Tailwind has its own system for these. Use Tailwind utility classes directly: -- Font sizes → \`text-[13px]\`, \`text-sm\`, \`text-base\`, etc. -- Font weights → \`font-bold\`, \`font-medium\`, \`font-[600]\` -- Spacing → \`gap-3\`, \`p-4\`, \`px-5\`, \`py-[14px]\`, or arbitrary \`gap-[12px]\` -- Border radius → \`rounded-xl\`, \`rounded-[14px]\`, \`rounded-full\` -Only create CSS custom properties for **semantic colors** — these are the values that would change across themes. Name them to avoid conflicts with Tailwind's built-in variables (do NOT use names like \`--font-bold\`, \`--text-sm\`, \`--radius-lg\`): +- Font sizes → `text-[13px]`, `text-sm`, `text-base`, etc. +- Font weights → `font-bold`, `font-medium`, `font-[600]` +- Spacing → `gap-3`, `p-4`, `px-5`, `py-[14px]`, or arbitrary `gap-[12px]` +- Border radius → `rounded-xl`, `rounded-[14px]`, `rounded-full` -\`\`\`css +Only create CSS custom properties for **semantic colors** — these are the values that would change across themes. Name them to avoid conflicts with Tailwind's built-in variables (do NOT use names like `--font-bold`, `--text-sm`, `--radius-lg`): + +```css :root { - --movie-bg: #0F0F1A; - --movie-surface: #1A1A2E; - --movie-accent: #7C3AED; - --movie-text: #FFFFFF; - --movie-text-dim: #FFFFFF80; + --movie-bg: #0f0f1a; + --movie-surface: #1a1a2e; + --movie-accent: #7c3aed; + --movie-text: #ffffff; + --movie-text-dim: #ffffff80; } -\`\`\` +``` -Reference in Tailwind classes: \`bg-[var(--movie-bg)]\`, \`text-[var(--movie-text)]\` +Reference in Tailwind classes: `bg-[var(--movie-bg)]`, `text-[var(--movie-text)]` #### CSS Modules / plain CSS projects Create CSS custom properties for all token categories (colors, spacing, typography, radius). Use a project-specific prefix to avoid collisions: -\`\`\`css +```css :root { - --app-color-bg: #0F0F1A; + --app-color-bg: #0f0f1a; --app-space-sm: 4px; --app-text-sm: 12px; --app-radius-md: 8px; } -\`\`\` +``` #### No design variables in the file -If the design has no Figma variables, extract implicit tokens from \`analyze_colors\` and \`analyze_typography\` output — identify the de facto palette and type scale. For Tailwind projects, only extract semantic colors as CSS custom properties; use Tailwind utilities for everything else. +If the design has no Figma variables, extract implicit tokens from `analyze_colors` and `analyze_typography` output — identify the de facto palette and type scale. For Tailwind projects, only extract semantic colors as CSS custom properties; use Tailwind utilities for everything else. #### Multi-mode collections (light/dark) - Generate token values for each mode -- Use CSS custom properties with class-based switching (\`.dark { ... }\`) +- Use CSS custom properties with class-based switching (`.dark { ... }`) ### Step 4 — Generate code For each component, bottom-up (primitives first, then composites, then screens): -\`\`\` +``` get_jsx id= → read structure describe id= → understand semantic role export_svg ids=[] → extract vector assets -\`\`\` +``` **Rules:** + - One component per file - Component name comes from the Figma node name, converted to PascalCase - Props interface reflects the variable content identified in Step 2 - Use design tokens from Step 3 for semantic colors -- For Tailwind: use utility classes directly for spacing, font sizes, weights, radius — do NOT wrap them in \`var()\` indirection +- For Tailwind: use utility classes directly for spacing, font sizes, weights, radius — do NOT wrap them in `var()` indirection - Match measurements exactly: font sizes, spacing, border radii, colors - Use auto-layout data to determine flex direction, gap, padding, alignment - Preserve text direction and container flow direction separately when the design uses RTL. -- Absolute positioning only when \`layoutPositioning\` is \`ABSOLUTE\` or layout mode is \`NONE\` -- If a node has \`clipsContent: true\`, use \`overflow: hidden\` +- Absolute positioning only when `layoutPositioning` is `ABSOLUTE` or layout mode is `NONE` +- If a node has `clipsContent: true`, use `overflow: hidden` - Text nodes: preserve font family, size, weight, line height, letter spacing, alignment -- Images/illustrations: use \`export_svg\` for vectors, placeholder \`\` for raster +- Images/illustrations: use `export_svg` for vectors, placeholder `` for raster **Interactive states:** Figma designs rarely include hover/active/focus states unless the component has explicit variants for them. Always add sensible interactive feedback to clickable elements: -- **Buttons (primary):** \`hover:brightness-110 active:brightness-90 transition-all\` -- **Buttons (secondary/ghost):** \`hover:bg-white/[0.12] active:bg-white/[0.06] transition-colors\` -- **Icon buttons:** \`hover:bg-white/[0.15] active:scale-95 transition-all\` -- **Cards/list items (if clickable):** \`hover:bg-white/[0.04] transition-colors\` -- **Links/text buttons:** \`hover:underline\` or \`hover:opacity-80\` -- **All interactive elements:** add \`cursor-pointer\` and \`select-none\` -- **Focus visible:** add \`focus-visible:ring-2 focus-visible:ring-offset-2\` with accent color for accessibility +- **Buttons (primary):** `hover:brightness-110 active:brightness-90 transition-all` +- **Buttons (secondary/ghost):** `hover:bg-white/[0.12] active:bg-white/[0.06] transition-colors` +- **Icon buttons:** `hover:bg-white/[0.15] active:scale-95 transition-all` +- **Cards/list items (if clickable):** `hover:bg-white/[0.04] transition-colors` +- **Links/text buttons:** `hover:underline` or `hover:opacity-80` +- **All interactive elements:** add `cursor-pointer` and `select-none` +- **Focus visible:** add `focus-visible:ring-2 focus-visible:ring-offset-2` with accent color for accessibility If the design HAS explicit hover/active variants (COMPONENT_SET with state property), use those exact styles instead of defaults above. @@ -142,12 +147,13 @@ If the design HAS explicit hover/active variants (COMPONENT_SET with state prope After generating code, verify against the design: -\`\`\` +``` describe id= → re-check structure matches get_jsx id= → compare JSX structure with generated component tree -\`\`\` +``` Check: + - All text content from the design appears in the code - All colors reference tokens or use correct hex/opacity values - Spacing values match the design @@ -160,18 +166,18 @@ List any deviations with rationale. The user specifies the target stack. Adapt code generation accordingly: -**React + Tailwind** — functional components, TypeScript, utility classes, \`className\` -**React + CSS Modules** — functional components, TypeScript, \`.module.css\` files, \`styles.className\` -**Vue 3 + Tailwind** — \`