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:

  1. Auto-Layout ↔ Column/Row. Figmas Auto-Layout-Frames (die überwiegende Mehrheit moderner Figma-Designs) werden als kind: "stack"-Nodes exportiert. Stack-Nodes haben eine axis-Eigenschaft — vertical mappt auf Column, horizontal mappt auf Row. Das ist ein 1:1-Mapping ohne Interpretationsschritt.
  2. Spacing-Tokens ↔ dp-Werte. Compose verwendet Dp für alle Layout-Dimensionen. Token-Werte in tokens.json sind einheitenlose ganze Zahlen (z.B. spacing.16 = 16), die direkt auf 16.dp abbilden. Keine Konvertierung, kein Skalieren.
  3. Farb-Tokens ↔ Color-Composable. Figma-Hex-Werte in tokens.json bilden mit einer einzigen Transformation auf Color(0xFFrrggbb) ab. Der Token-Key wird zu einem semantischen Variablennamen im Theme.

Die IR-Node-Arten und ihre Compose-Mappings

IR-ArtEigenschaftenCompose-Primitiv
stackaxis: "vertical"Column
stackaxis: "horizontal"Row
overlaygeschichtete KinderBox
absolutex, y, width, heightBox mit Modifier.offset(x.dp, y.dp)
leaftype: "text"Text mit TextStyle
leaftype: "rectangle" mit FillBox(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:

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.titleR.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:

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.