El objetivo de exportación predeterminado de figmascope es Jetpack Compose. Eso no es arbitrario — el modelo de layout de Compose (Column, Row, Box, Modifier) se mapea estrechamente a los tipos de nodos IR (stack, overlay, absolute, leaf). Un stack vertical en Figma es un Column en Compose. La traducción es mecánica, lo que la hace bien adecuada para el codegen impulsado por agentes.
Este recorrido cubre el mapeo de IR a Compose en detalle, muestra un fragmento JSON real con el Composable correspondiente y explica la capa de mapeo de tokens.
Por qué Compose es el objetivo predeterminado
Tres razones estructurales:
- Auto-layout ↔ Column/Row. Los frames de auto-layout de Figma (la gran mayoría de los diseños modernos de Figma) se exportan como nodos
kind: "stack". Los nodos stack tienen una propiedadaxis—verticalse mapea aColumn,horizontalse mapea aRow. Este es un mapeo 1:1 sin paso de interpretación. - Tokens de espaciado ↔ valores dp. Compose usa
Dppara todas las dimensiones de layout. Los valores de tokens entokens.jsonson enteros sin unidad (por ejemplo,spacing.16 = 16) que se mapean directamente a16.dp. Sin conversión, sin escala. - Tokens de color ↔ Color composable. Los valores hexadecimales de Figma en
tokens.jsonse mapean aColor(0xFFrrggbb)con una sola transformación. La clave del token se convierte en un nombre de variable semántico en tu tema.
Los tipos de nodos IR y sus mapeos en Compose
| Tipo IR | Propiedades | Primitiva Compose |
|---|---|---|
stack | axis: "vertical" | Column |
stack | axis: "horizontal" | Row |
overlay | hijos superpuestos | Box |
absolute | x, y, width, height | Box con Modifier.offset(x.dp, y.dp) |
leaf | type: "text" | Text con TextStyle |
leaf | type: "rectangle" con fill | Box(Modifier.background(Color(...))) |
Todos los tipos de nodos pueden llevar una propiedad spacing (separación entre hijos) y un objeto padding (arriba/derecha/abajo/izquierda). Ambos referencian claves de tokens.
Mapeo de tokens en detalle
Un archivo tokens.json se ve así:
{
"spacing": {
"4": 4, "8": 8, "12": 12, "16": 16,
"20": 20, "24": 24, "32": 32, "48": 48
},
"radius": {
"4": 4, "8": 8, "12": 12, "16": 16, "full": 9999
},
"color": {
"7f5cfe": "#7F5CFE",
"1a1a2e": "#1A1A2E",
"f6f2ea": "#F6F2EA",
"ffffff": "#FFFFFF",
"e53935": "#E53935"
},
"typography": {
"heading.24": { "size": 24, "weight": 700, "lineHeight": 1.2 },
"body.14": { "size": 14, "weight": 400, "lineHeight": 1.5 },
"label.12": { "size": 12, "weight": 500, "lineHeight": 1.4 }
}
}
Las reglas de mapeo:
spacing.16→16.dpradius.12→RoundedCornerShape(12.dp)color.7f5cfe→Color(0xFF7F5CFE)(añadir0xFFpara alpha completo)typography.heading.24→TextStyle(fontSize = 24.sp, fontWeight = FontWeight.Bold, lineHeight = 28.8.sp)
Las claves de tokens son intencionalmente opacas (cadenas hexadecimales para colores, cadenas numéricas para espaciado) para no sesgar al modelo hacia ninguna convención de nomenclatura particular. Tu tema de Compose puede crear alias semánticos — colorPrimary, spacingMd — independientemente del IR.
Un ejemplo real: JSON de pantalla de inicio a Composable
Aquí hay un IR simplificado de pantalla de inicio. Un stack vertical con una hoja de encabezado y una lista de tarjetas:
{
"name": "home",
"kind": "stack",
"axis": "vertical",
"spacing": "spacing.24",
"padding": { "top": "spacing.16", "right": "spacing.16",
"bottom": "spacing.16", "left": "spacing.16" },
"fill": "color.f6f2ea",
"children": [
{
"kind": "leaf",
"type": "text",
"stringRef": "home.title",
"typography": "typography.heading.24",
"fill": "color.1a1a2e"
},
{
"kind": "stack",
"axis": "vertical",
"spacing": "spacing.12",
"children": [
{
"kind": "overlay",
"radius": "radius.12",
"fill": "color.ffffff",
"padding": { "top": "spacing.16", "right": "spacing.16",
"bottom": "spacing.16", "left": "spacing.16" },
"children": [
{
"kind": "leaf",
"type": "text",
"stringRef": "home.card.label",
"typography": "typography.label.12",
"fill": "color.7f5cfe"
}
]
}
]
}
]
}
El Composable correspondiente — lo que un agente debería producir desde este IR:
@Composable
fun HomeScreen() {
Column(
modifier = Modifier
.fillMaxSize()
.background(Color(0xFFF6F2EA))
.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(24.dp)
) {
Text(
text = stringResource(R.string.home_title),
style = TextStyle(
fontSize = 24.sp,
fontWeight = FontWeight.Bold,
lineHeight = 28.8.sp,
color = Color(0xFF1A1A2E)
)
)
Column(
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
Box(
modifier = Modifier
.clip(RoundedCornerShape(12.dp))
.background(Color(0xFFFFFFFF))
.padding(16.dp)
) {
Text(
text = stringResource(R.string.home_card_label),
style = TextStyle(
fontSize = 12.sp,
fontWeight = FontWeight.Medium,
lineHeight = 16.8.sp,
color = Color(0xFF7F5CFE)
)
)
}
}
}
}
Cada valor en el Composable se remonta a una clave de token en el IR. Nada está hardcodeado — 16.dp viene de spacing.16, 24.dp de spacing.24, Color(0xFF7F5CFE) de color.7f5cfe.
Refs de cadenas — mapeo a stringResource
Cada nodo de texto en el IR lleva un stringRef con una clave en notación de puntos. El archivo strings.json mapea claves a valores de visualización y fallbacks:
{
"home.title": { "value": "Good morning", "fallback": "Good morning" },
"home.card.label": { "value": "Today's summary", "fallback": "Summary" }
}
La notación de puntos se mapea a IDs de recursos de cadenas de Android con los puntos reemplazados por guiones bajos: home.title → R.string.home_title. El campo fallback es lo que hardcodeas como la cadena literal si el recurso aún no existe en strings.xml:
text = stringResource(R.string.home_title, "Good morning")
Dile al agente que siempre use el campo fallback — no una cadena vacía — para que la pantalla sea legible durante el desarrollo antes de que se pueble strings.xml.
Posicionamiento absoluto
Los nodos con kind: "absolute" usan coordenadas de Figma directamente. Aparecen en diseños con elementos superpuestos o anclados a posiciones específicas. El mapeo en Compose usa Box como padre y Modifier.offset en los hijos:
// IR: { "kind": "absolute", "x": 24, "y": 80, "width": 120, "height": 40 }
Box(modifier = Modifier.fillMaxSize()) {
Box(
modifier = Modifier
.offset(x = 24.dp, y = 80.dp)
.size(width = 120.dp, height = 40.dp)
) {
// children
}
}
El posicionamiento absoluto es poco común en archivos de Figma bien estructurados (la mayoría de los archivos modernos usan auto-layout). Cuando lo veas, verifica si la intención del diseño es verdaderamente absoluta o si el diseñador simplemente no aplicó restricciones de auto-layout — el IR no puede inferir la intención.
Brechas honestas
El bundle cubre bien el caso común. Algunas cosas que no cubre:
- Rellenos de degradado. El IR exporta una advertencia cuando encuentra un degradado. El relleno de fondo del nodo se omite. Deja un
// TODO: gradiente impleméntalo manualmente. - Efectos complejos. Las sombras y los desenfocados no están representados en el IR. Aparecen en las advertencias de
_meta.jsonsi están presentes. - Iconos vectoriales. El IR almacena un ID de referencia para los nodos de iconos, no datos de ruta. Necesitarás resolver el icono a un drawable real o un icono de Compose por separado.
- Componentes anidados. El IR incluye
componentIden los nodos de instancia.components/inventory.jsonmapea IDs a nombres. Implementa el componente por separado y referéncialo por nombre en el Composable padre.
Estas brechas son explícitas — aparecen en las advertencias de _meta.json y en CONTEXT.md. El agente no las adivina silenciosamente.
Exporta el bundle de contexto desde la app principal de figmascope, luego úsalo con Claude Code o Cursor para implementar Composables directamente desde el IR. Sin adivinanzas a partir de capturas de pantalla, sin valores hardcodeados.