Il target di esportazione predefinito di figmascope è Jetpack Compose. Non è arbitrario — il modello di layout di Compose (Column, Row, Box, Modifier) si mappa strettamente ai tipi di nodi IR (stack, overlay, absolute, leaf). Uno stack verticale in Figma è una Column in Compose. La traduzione è meccanica, il che la rende adatta alla codegen guidata da agenti.
Questa guida copre in dettaglio il mapping IR-to-Compose, mostra un frammento JSON reale con il Composable corrispondente e spiega il layer di mapping dei token.
Perché Compose è il target predefinito
Tre ragioni strutturali:
- Auto-layout ↔ Column/Row. I frame auto-layout di Figma (la stragrande maggioranza dei design Figma moderni) vengono esportati come nodi
kind: "stack". I nodi stack hanno una proprietàaxis—verticalsi mappa aColumn,horizontalsi mappa aRow. Questo è un mapping 1:1 senza nessun passaggio di interpretazione. - Token di spaziatura ↔ valori dp. Compose usa
Dpper tutte le dimensioni del layout. I valori dei token intokens.jsonsono interi senza unità (ad es.,spacing.16 = 16) che si mappano direttamente a16.dp. Nessuna conversione, nessun ridimensionamento. - Token di colore ↔ composable Color. I valori hex di Figma in
tokens.jsonsi mappano aColor(0xFFrrggbb)con una singola trasformazione. La chiave del token diventa un nome di variabile semantica nel tuo tema.
I tipi di nodi IR e i loro mapping Compose
| Tipo IR | Proprietà | Primitiva Compose |
|---|---|---|
stack | axis: "vertical" | Column |
stack | axis: "horizontal" | Row |
overlay | figli sovrapposti | Box |
absolute | x, y, width, height | Box con Modifier.offset(x.dp, y.dp) |
leaf | type: "text" | Text con TextStyle |
leaf | type: "rectangle" con riempimento | Box(Modifier.background(Color(...))) |
Tutti i tipi di nodi possono portare una proprietà spacing (gap tra i figli) e un oggetto padding (top/right/bottom/left). Entrambi fanno riferimento a chiavi di token.
Mapping dei token in dettaglio
Un file tokens.json appare così:
{
"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 }
}
}
Le regole di mapping:
spacing.16→16.dpradius.12→RoundedCornerShape(12.dp)color.7f5cfe→Color(0xFF7F5CFE)(anteponi0xFFper alpha piena)typography.heading.24→TextStyle(fontSize = 24.sp, fontWeight = FontWeight.Bold, lineHeight = 28.8.sp)
Le chiavi dei token sono intenzionalmente opache (stringhe hex per il colore, stringhe numeriche per la spaziatura) in modo che non pregiudichino il modello verso alcuna convenzione di denominazione particolare. Il tuo tema Compose può creare alias con nomi semantici — colorPrimary, spacingMd — indipendentemente dall'IR.
Un esempio reale: da JSON della schermata home a Composable
Ecco un IR semplificato della schermata home. Uno stack verticale con un leaf header e una lista di card:
{
"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"
}
]
}
]
}
]
}
Il Composable corrispondente — quello che un agente dovrebbe produrre da questo 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)
)
)
}
}
}
}
Ogni valore nel Composable risale a una chiave token nell'IR. Nulla è hardcoded — 16.dp viene da spacing.16, 24.dp da spacing.24, Color(0xFF7F5CFE) da color.7f5cfe.
Riferimenti alle stringhe — mapping stringResource
Ogni nodo di testo nell'IR porta un stringRef con una chiave in notazione punto. Il file strings.json mappa le chiavi ai valori di visualizzazione e ai fallback:
{
"home.title": { "value": "Buongiorno", "fallback": "Buongiorno" },
"home.card.label": { "value": "Riepilogo di oggi", "fallback": "Riepilogo" }
}
La notazione punto si mappa agli ID delle risorse stringa Android con i punti sostituiti da underscore: home.title → R.string.home_title. Il campo fallback è quello che hardcodi come stringa letterale se la risorsa non esiste ancora in strings.xml:
text = stringResource(R.string.home_title, "Buongiorno")
Di' all'agente di usare sempre il campo fallback — non una stringa vuota — in modo che la schermata sia leggibile durante lo sviluppo prima che strings.xml venga popolato.
Posizionamento assoluto
I nodi con kind: "absolute" usano direttamente le coordinate Figma. Compaiono nei design con elementi sovrapposti o ancorati a posizioni specifiche. Il mapping Compose usa Box come genitore e Modifier.offset sui figli:
// 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)
) {
// figli
}
}
Il posizionamento assoluto è raro nei file Figma ben strutturati (la maggior parte dei file moderni usa auto-layout). Quando lo vedi, verifica se l'intento del design è veramente assoluto o se il designer semplicemente non ha applicato i vincoli auto-layout — l'IR non può inferire l'intento.
Lacune oneste
Il bundle copre bene il caso comune. Alcune cose che non copre:
- Riempimenti a gradiente. L'IR esporta un avviso quando incontra un gradiente. Il riempimento dello sfondo del nodo viene omesso. Lascia un
// TODO: gradientee implementalo manualmente. - Effetti complessi. Le ombre esterne e le sfocature non sono rappresentate nell'IR. Compaiono negli avvisi di
_meta.jsonse presenti. - Icone vettoriali. L'IR memorizza un ID di riferimento per i nodi icona, non i dati del percorso. Dovrai risolvere l'icona in un drawable effettivo o in un'icona compose separatamente.
- Componenti annidati. L'IR include
componentIdsui nodi istanza.components/inventory.jsonmappa gli ID ai nomi. Implementa il componente separatamente e fai riferimento ad esso per nome nel Composable genitore.
Queste lacune sono esplicite — compaiono negli avvisi di _meta.json e in CONTEXT.md. L'agente non le attraversa silenziosamente indovinando.
Esporta il bundle di contesto dall'app figmascope principale, poi usalo con Claude Code o Cursor per implementare i Composable direttamente dall'IR. Nessun guessing da screenshot, nessun valore hardcoded.