La cible d'export par défaut de figmascope est Jetpack Compose. Ce n'est pas arbitraire — le modèle de mise en page de Compose (Column, Row, Box, Modifier) se mappe étroitement sur les types de nœuds IR (stack, overlay, absolute, leaf). Un stack vertical dans Figma est une Column dans Compose. La traduction est mécanique, ce qui la rend bien adaptée à la génération de code pilotée par un agent.
Ce tutoriel couvre en détail le mapping IR vers Compose, montre un fragment JSON réel avec le Composable correspondant, et explique la couche de mapping des tokens.
Pourquoi Compose est la cible par défaut
Trois raisons structurelles :
- Auto-layout ↔ Column/Row. Les frames auto-layout de Figma (la grande majorité des designs Figma modernes) s'exportent comme des nœuds
kind: "stack". Les nœuds stack ont une propriétéaxis—verticalse mappe surColumn,horizontalsurRow. C'est un mapping 1:1 sans étape d'interprétation. - Tokens d'espacement ↔ valeurs dp. Compose utilise
Dppour toutes les dimensions de mise en page. Les valeurs de tokens danstokens.jsonsont des entiers sans unité (par ex.spacing.16 = 16) qui se mappent directement sur16.dp. Pas de conversion, pas de mise à l'échelle. - Tokens de couleur ↔ Color Composable. Les valeurs hex Figma dans
tokens.jsonse mappent surColor(0xFFrrggbb)avec une seule transformation. La clé du token devient un nom de variable sémantique dans votre thème.
Les types de nœuds IR et leurs mappings Compose
| Type IR | Propriétés | Primitif Compose |
|---|---|---|
stack | axis: "vertical" | Column |
stack | axis: "horizontal" | Row |
overlay | enfants superposés | Box |
absolute | x, y, width, height | Box avec Modifier.offset(x.dp, y.dp) |
leaf | type: "text" | Text avec TextStyle |
leaf | type: "rectangle" avec fill | Box(Modifier.background(Color(...))) |
Tous les types de nœuds peuvent porter une propriété spacing (espace entre les enfants) et un objet padding (haut/droite/bas/gauche). Les deux référencent des clés de tokens.
Le mapping des tokens en détail
Un fichier tokens.json ressemble à ceci :
{
"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 }
}
}
Les règles de mapping :
spacing.16→16.dpradius.12→RoundedCornerShape(12.dp)color.7f5cfe→Color(0xFF7F5CFE)(préfixe0xFFpour l'alpha complet)typography.heading.24→TextStyle(fontSize = 24.sp, fontWeight = FontWeight.Bold, lineHeight = 28.8.sp)
Les clés de tokens sont intentionnellement opaques (chaînes hex pour les couleurs, chaînes numériques pour l'espacement) pour ne pas biaiser le modèle vers une convention de nommage particulière. Votre thème Compose peut les alias en noms sémantiques — colorPrimary, spacingMd — indépendamment de l'IR.
Un exemple réel : écran d'accueil JSON vers Composable
Voici un IR d'écran d'accueil simplifié. Un stack vertical avec un leaf d'en-tête et une liste de cartes :
{
"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"
}
]
}
]
}
]
}
Le Composable correspondant — ce qu'un agent devrait produire depuis cet 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)
)
)
}
}
}
}
Chaque valeur dans le Composable est traçable jusqu'à une clé de token dans l'IR. Rien n'est codé en dur — 16.dp vient de spacing.16, 24.dp de spacing.24, Color(0xFF7F5CFE) de color.7f5cfe.
Refs de chaînes — mapping stringResource
Chaque nœud texte dans l'IR porte un stringRef avec une clé en notation pointée. Le fichier strings.json mappe les clés sur des valeurs d'affichage et des fallbacks :
{
"home.title": { "value": "Good morning", "fallback": "Good morning" },
"home.card.label": { "value": "Today's summary", "fallback": "Summary" }
}
La notation pointée se mappe sur les IDs de ressources de chaînes Android avec les points remplacés par des tirets bas : home.title → R.string.home_title. Le champ fallback est ce que vous codez en dur comme chaîne littérale si la ressource n'existe pas encore dans strings.xml :
text = stringResource(R.string.home_title, "Good morning")
Demandez à l'agent d'utiliser toujours le champ fallback — pas une chaîne vide — pour que l'écran soit lisible pendant le développement avant que strings.xml soit rempli.
Positionnement absolu
Les nœuds avec kind: "absolute" utilisent les coordonnées Figma directement. Ces nœuds apparaissent dans les designs avec des éléments qui se chevauchent ou ancrés à des positions spécifiques. Le mapping Compose utilise Box comme parent et Modifier.offset sur les enfants :
// 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)
) {
// enfants
}
}
Le positionnement absolu est peu courant dans les fichiers Figma bien structurés (la plupart des fichiers modernes utilisent l'auto-layout). Quand vous le voyez, vérifiez si l'intention de design est vraiment absolue ou si le designer n'a simplement pas appliqué les contraintes d'auto-layout — l'IR ne peut pas inférer l'intention.
Lacunes honnêtes
Le bundle couvre bien le cas courant. Quelques choses qu'il ne couvre pas :
- Remplissages dégradés. L'IR exporte un avertissement quand il rencontre un dégradé. Le remplissage d'arrière-plan du nœud est omis. Laissez un
// TODO: gradientet implémentez-le manuellement. - Effets complexes. Les ombres portées et les flous ne sont pas représentés dans l'IR. Ils apparaissent dans les avertissements de
_meta.jsons'ils sont présents. - Icônes vectorielles. L'IR stocke un ID de référence pour les nœuds d'icônes, pas les données de chemin. Vous devrez résoudre l'icône en un drawable ou une icône Compose réel séparément.
- Composants imbriqués. L'IR inclut
componentIdsur les nœuds d'instance.components/inventory.jsonmappe les IDs sur les noms. Implémentez le composant séparément et référencez-le par nom dans le Composable parent.
Ces lacunes sont explicites — elles apparaissent dans les avertissements de _meta.json et dans CONTEXT.md. L'agent ne les contourne pas silencieusement en devinant.
Exportez le bundle de contexte depuis l'application principale figmascope, puis utilisez-le avec Claude Code ou Cursor pour implémenter des Composables directement depuis l'IR. Pas de devinette par capture d'écran, pas de valeurs codées en dur.