Domyślnym celem eksportu figmascope jest Jetpack Compose. To nie jest przypadek — model układu Compose (Column, Row, Box, Modifier) ściśle mapuje na rodzaje węzłów IR (stack, overlay, absolute, leaf). Pionowy stack w Figmie to Column w Compose. Tłumaczenie jest mechaniczne, co czyni go dobrze dostosowanym do codegenu sterowanego przez agenta.

Ten przewodnik szczegółowo omawia mapowanie IR-na-Compose, pokazuje prawdziwy fragment JSON z odpowiednim Composable i wyjaśnia warstwę mapowania tokenów.

Dlaczego Compose jest domyślnym celem

Trzy strukturalne powody:

  1. Auto-layout ↔ Column/Row. Ramki auto-layout Figmy (zdecydowana większość nowoczesnych projektów Figma) eksportują jako węzły kind: "stack". Węzły stack mają właściwość axisvertical mapuje na Column, horizontal mapuje na Row. To mapowanie 1:1 bez kroku interpretacji.
  2. Tokeny odstępów ↔ wartości dp. Compose używa Dp dla wszystkich wymiarów układu. Wartości tokenów w tokens.json to bezjednostkowe liczby całkowite (np. spacing.16 = 16), które mapują bezpośrednio na 16.dp. Bez konwersji, bez skalowania.
  3. Tokeny kolorów ↔ Color composable. Wartości hex Figmy w tokens.json mapują na Color(0xFFrrggbb) z jedną transformacją. Klucz tokenu staje się semantyczną nazwą zmiennej w Twoim motywie.

Rodzaje węzłów IR i ich mapowania Compose

Rodzaj IRWłaściwościPrymityw Compose
stackaxis: "vertical"Column
stackaxis: "horizontal"Row
overlaywarstwowane dzieciBox
absolutex, y, width, heightBox z Modifier.offset(x.dp, y.dp)
leaftype: "text"Text z TextStyle
leaftype: "rectangle" z wypełnieniemBox(Modifier.background(Color(...)))

Wszystkie rodzaje węzłów mogą nosić właściwość spacing (przerwa między dziećmi) i obiekt padding (góra/prawo/dół/lewo). Obie odwołują się do kluczy tokenów.

Mapowanie tokenów w szczegółach

Plik tokens.json wygląda tak:

{
  "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 }
  }
}

Reguły mapowania:

Klucze tokenów są celowo nieprzezroczyste (ciągi hex dla koloru, ciągi numeryczne dla odstępów), żeby nie nastawiały modelu na żadną konkretną konwencję nazewnictwa. Twój motyw Compose może aliasować je do semantycznych nazw — colorPrimary, spacingMd — niezależnie od IR.

Realny przykład: ekran główny JSON na Composable

Oto uproszczony IR ekranu głównego. Pionowy stack z nagłówkiem leaf i listą kart:

{
  "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"
            }
          ]
        }
      ]
    }
  ]
}

Odpowiadający Composable — co agent powinien wyprodukować z tego 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)
                    )
                )
            }
        }
    }
}

Każda wartość w Composable daje się prześledzić do klucza tokenu w IR. Nic nie jest zahardkodowane — 16.dp pochodzi z spacing.16, 24.dp z spacing.24, Color(0xFF7F5CFE) z color.7f5cfe.

Referencje ciągów — mapowanie stringResource

Każdy węzeł tekstowy w IR niesie stringRef z kluczem w notacji z kropką. Plik strings.json mapuje klucze na wartości wyświetlane i fallbacki:

{
  "home.title": { "value": "Dzień dobry", "fallback": "Good morning" },
  "home.card.label": { "value": "Dzisiejsze podsumowanie", "fallback": "Summary" }
}

Notacja z kropką mapuje na ID zasobów stringów Androida z kropkami zamienionymi na podkreślniki: home.titleR.string.home_title. Pole fallback to to, co zahardkodowujesz jako literał, jeśli zasób nie istnieje jeszcze w strings.xml:

text = stringResource(R.string.home_title, "Good morning")

Powiedz agentowi, by zawsze używał pola fallback — nie pustego ciągu — żeby ekran był czytelny podczas dewelopmentu, zanim strings.xml zostanie wypełniony.

Pozycjonowanie absolutne

Węzły z kind: "absolute" używają bezpośrednio współrzędnych Figmy. Pojawiają się w projektach z nakładającymi się elementami lub elementami zakotwiczonymi na konkretnych pozycjach. Mapowanie Compose używa Box jako rodzica i Modifier.offset na dzieciach:

// 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)
    ) {
        // dzieci
    }
}

Pozycjonowanie absolutne jest rzadkie w dobrze ustrukturyzowanych plikach Figma (większość nowoczesnych plików używa auto-layout). Gdy je widzisz, sprawdź, czy intencją projektu naprawdę jest pozycjonowanie absolutne, czy projektant po prostu nie zastosował ograniczeń auto-layout — IR nie może wnioskować intencji.

Uczciwe luki

Pakiet dobrze pokrywa typowy przypadek. Kilka rzeczy, których nie pokrywa:

Te luki są explicite — pojawiają się w ostrzeżeniach _meta.json i CONTEXT.md. Agent nie zgaduje przez nie po cichu.

Wyeksportuj pakiet kontekstowy z głównej aplikacji figmascope, a następnie użyj go z Claude Code lub Cursor, by implementować Composable bezpośrednio z IR. Bez zgadywania ze zrzutów ekranu, bez zahardkodowanych wartości.