Das Standard-Exportziel von figmascope ist Jetpack Compose. Das ist nicht willkürlich — Composes Layout-Modell (Column, Row, Box, Modifier) bildet eng auf die IR-Node-Arten (stack, overlay, absolute, leaf) ab. Ein vertikaler Stack in Figma ist eine Column in Compose. Die Übersetzung ist mechanisch, was sie gut für agentengesteuerten Codegen geeignet macht.
Dieser Walkthrough deckt das IR-zu-Compose-Mapping im Detail ab, zeigt ein echtes JSON-Fragment mit dem entsprechenden Composable und erklärt die Token-Mapping-Schicht.
Warum Compose das Standard-Ziel ist
Drei strukturelle Gründe:
- Auto-Layout ↔ Column/Row. Figmas Auto-Layout-Frames (die überwiegende Mehrheit moderner Figma-Designs) werden als
kind: "stack"-Nodes exportiert. Stack-Nodes haben eineaxis-Eigenschaft —verticalmappt aufColumn,horizontalmappt aufRow. Das ist ein 1:1-Mapping ohne Interpretationsschritt. - Spacing-Tokens ↔ dp-Werte. Compose verwendet
Dpfür alle Layout-Dimensionen. Token-Werte intokens.jsonsind einheitenlose ganze Zahlen (z.B.spacing.16 = 16), die direkt auf16.dpabbilden. Keine Konvertierung, kein Skalieren. - Farb-Tokens ↔ Color-Composable. Figma-Hex-Werte in
tokens.jsonbilden mit einer einzigen Transformation aufColor(0xFFrrggbb)ab. Der Token-Key wird zu einem semantischen Variablennamen im Theme.
Die IR-Node-Arten und ihre Compose-Mappings
| IR-Art | Eigenschaften | Compose-Primitiv |
|---|---|---|
stack | axis: "vertical" | Column |
stack | axis: "horizontal" | Row |
overlay | geschichtete Kinder | Box |
absolute | x, y, width, height | Box mit Modifier.offset(x.dp, y.dp) |
leaf | type: "text" | Text mit TextStyle |
leaf | type: "rectangle" mit Fill | Box(Modifier.background(Color(...))) |
Alle Node-Arten können eine spacing-Eigenschaft (Abstand zwischen Kindern) und ein padding-Objekt (oben/rechts/unten/links) tragen. Beide referenzieren Token-Keys.
Token-Mapping im Detail
Eine tokens.json-Datei sieht so aus:
{
"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 }
}
}
Die Mapping-Regeln:
spacing.16→16.dpradius.12→RoundedCornerShape(12.dp)color.7f5cfe→Color(0xFF7F5CFE)(0xFFfür vollen Alpha voranstellen)typography.heading.24→TextStyle(fontSize = 24.sp, fontWeight = FontWeight.Bold, lineHeight = 28.8.sp)
Die Token-Keys sind absichtlich opak (Hex-Strings für Farbe, numerische Strings für Spacing), damit sie das Modell nicht zu einer bestimmten Benennungskonvention hin voreingenommen machen. Das Compose-Theme kann sie unabhängig vom IR auf semantische Namen aliasieren — colorPrimary, spacingMd.
Ein echtes Beispiel: Home-Screen-JSON zu Composable
Hier ist ein vereinfachtes Home-Screen-IR. Ein vertikaler Stack mit einem Header-Leaf und einer Kartenliste:
{
"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"
}
]
}
]
}
]
}
Das entsprechende Composable — was ein Agent aus diesem IR produzieren sollte:
@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)
)
)
}
}
}
}
Jeder Wert im Composable ist auf einen Token-Key im IR zurückverfolgbar. Nichts ist hartcodiert — 16.dp kommt aus spacing.16, 24.dp aus spacing.24, Color(0xFF7F5CFE) aus color.7f5cfe.
String-Refs — stringResource-Mapping
Jeder Text-Node im IR trägt einen stringRef mit einem Punkt-Notation-Key. Die strings.json-Datei mappt Keys auf Anzeigewerte und Fallbacks:
{
"home.title": { "value": "Guten Morgen", "fallback": "Guten Morgen" },
"home.card.label": { "value": "Heutige Zusammenfassung", "fallback": "Zusammenfassung" }
}
Die Punkt-Notation bildet auf Android-String-Ressourcen-IDs ab, wobei Punkte durch Unterstriche ersetzt werden: home.title → R.string.home_title. Das fallback-Feld ist das, was man als Literal-String hardcodet, wenn die Ressource noch nicht in strings.xml existiert:
text = stringResource(R.string.home_title, "Guten Morgen")
Den Agenten anweisen, immer das Fallback-Feld zu verwenden — nicht einen leeren String — damit der Screen während der Entwicklung lesbar ist, bevor strings.xml befüllt ist.
Absolute Positionierung
Nodes mit kind: "absolute" verwenden Figma-Koordinaten direkt. Diese erscheinen in Designs mit überlappenden Elementen oder Elementen, die an bestimmten Positionen verankert sind. Das Compose-Mapping verwendet Box als Elternelement und Modifier.offset auf Kindern:
// 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)
) {
// Kinder
}
}
Absolute Positionierung ist in gut strukturierten Figma-Dateien ungewöhnlich (die meisten modernen Dateien verwenden Auto-Layout). Wenn man sie sieht, sollte man überprüfen, ob die Design-Intention wirklich absolut ist oder ob der Designer einfach keine Auto-Layout-Constraints angewendet hat — das IR kann die Intention nicht ableiten.
Ehrliche Lücken
Das Bundle deckt den häufigen Fall gut ab. Ein paar Dinge, die es nicht abdeckt:
- Verlaufsfüllungen. Das IR exportiert eine Warnung, wenn es auf einen Verlauf trifft. Der Hintergrundfill des Nodes wird weggelassen. Ein
// TODO: Verlaufhinterlassen und manuell implementieren. - Komplexe Effekte. Drop-Shadows und Blur sind nicht im IR repräsentiert. Sie erscheinen in den
_meta.json-Warnungen wenn vorhanden. - Vektor-Icons. Das IR speichert eine Referenz-ID für Icon-Nodes, keine Pfaddaten. Das Icon muss separat zu einem tatsächlichen Drawable oder Compose-Icon aufgelöst werden.
- Verschachtelte Komponenten. Das IR enthält
componentIdauf Instanz-Nodes.components/inventory.jsonmappt IDs auf Namen. Die Komponente separat implementieren und im übergeordneten Composable per Namen referenzieren.
Diese Lücken sind explizit — sie erscheinen in _meta.json-Warnungen und in CONTEXT.md. Der Agent rät nicht still darüber hinweg.
Das Kontext-Bundle aus der figmascope-Haupt-App exportieren und dann mit Claude Code oder Cursor verwenden, um Composables direkt aus dem IR zu implementieren. Kein Screenshot-Raten, keine hartcodierten Werte.