L'albero dei nodi di Figma è ricco ma rumoroso. Frame, Group, Component, Instance, Text, Rectangle, Ellipse, Vector — ognuno ha decine di campi opzionali, alcuni dei quali sono in conflitto tra loro. Un agente che lavora direttamente dalla risposta dell'API Figma grezza spende budget cognitivo a capire lo schema invece di scrivere codice.

figmascope normalizza l'albero in quattro tipi di nodi: stack, overlay, absolute e leaf. Ogni nodo in ogni file screens/*.json è uno di questi quattro. Nient'altro.

I quattro tipi

stack

Uno stack è un nodo con Figma Auto Layout — layoutMode: "VERTICAL" o layoutMode: "HORIZONTAL". I suoi figli sono posizionati in flusso lungo l'asse. Gap e padding sono espliciti.

{
  "kind": "stack",
  "id": "123:456",
  "name": "ContentColumn",
  "axis": "vertical",
  "gap": 16,
  "paddingTop": 24,
  "paddingBottom": 24,
  "paddingLeft": 20,
  "paddingRight": 20,
  "primaryAxisAlignItems": "SPACE_BETWEEN",
  "counterAxisAlignItems": "CENTER",
  "width": 390,
  "height": 844,
  "fills": [{ "type": "SOLID", "color": "#1a1a2e" }],
  "children": [ ... ]
}

Si mappa a Jetpack Compose come Column (asse verticale) o Row (asse orizzontale). I campi primaryAxisAlignItems e counterAxisAlignItems si mappano rispettivamente a Arrangement e Alignment. gap diventa Arrangement.spacedBy().

overlay

Un overlay è un nodo i cui figli hanno posizioni assolute al suo interno. Figma lo rappresenta come un Frame con layoutMode: "NONE" dove i figli hanno coordinate absoluteBoundingBox. Il tipo overlay cattura questo senza richiedere all'agente di ragionare sulle coordinate grezze per capire la struttura.

{
  "kind": "overlay",
  "id": "123:789",
  "name": "CardOverlay",
  "width": 358,
  "height": 200,
  "fills": [{ "type": "SOLID", "color": "#ffffff" }],
  "children": [
    {
      "kind": "absolute",
      "offset": { "x": 16, "y": 16 },
      "child": { ... }
    }
  ]
}

Si mappa a Compose come Box. I figli sono posizionati usando Modifier.offset() o Modifier.align() a seconda di come le loro posizioni si relazionano ai limiti del genitore.

absolute

Un nodo absolute è un wrapper che porta un singolo figlio a un offset specifico (x, y) all'interno del suo overlay genitore. È un nodo strutturale leggero — esiste per preservare la relazione spaziale da Figma senza confondere posizione e contenuto.

{
  "kind": "absolute",
  "offset": { "x": 24, "y": 140 },
  "child": {
    "kind": "leaf",
    "name": "BadgeLabel",
    "text": "NUOVO",
    "stringRef": "badge.new",
    ...
  }
}

In Compose, questo tipicamente diventa un figlio di un Box con Modifier.offset(x.dp, y.dp). I valori di offset sono candidati per la sostituzione con token se esistono token di spaziatura nell'intervallo.

leaf

Un leaf è un nodo terminale — nessun figlio. Ha stile (riempimenti, bordi, corner radius) e opzionalmente contenuto testuale. I leaf di testo hanno un campo text (la stringa letterale) e un campo stringRef (la chiave della risorsa i18n da strings.json).

{
  "kind": "leaf",
  "id": "123:101",
  "name": "SpeedLabel",
  "text": "Speed Test",
  "stringRef": "speed.test",
  "fontSize": 18,
  "fontWeight": 600,
  "fills": [{ "type": "SOLID", "color": "#ffffff" }],
  "width": 160,
  "height": 28
}

Si mappa a Compose come composable Text() per il contenuto testuale, o come Surface, Box o Image per i leaf non testuali a seconda del tipo di riempimento.

I quattro tipi si mappano uno-a-uno al modello compositivo di qualsiasi framework UI. Gli stack sono layout a flusso. Gli overlay sono contenitori posizionati lungo l'asse z. Gli absolute portano metadati spaziali. I leaf sono contenuto. Tutto qui. La tassonomia dei nodi Figma ha 12+ tipi di nodi — l'IR li riduce a quattro.

Come viene determinato il tipo

La logica di classificazione:

  1. Se layoutMode è "VERTICAL" o "HORIZONTAL"stack
  2. Se layoutMode è "NONE" e il nodo ha figli → overlay (figli avvolti come absolute)
  3. Se un nodo è il figlio diretto di un overlay e porta una posizione → absolute
  4. Se il nodo non ha figli e non è un wrapper absolute → leaf

L'avviso layout-mode-none-inferred in _meta.json si attiva quando un Frame con layoutMode: "NONE" ha figli con bounding box che si sovrappongono in modo non banale. figmascope lo tratta come un overlay, ma annota l'inferenza perché alcuni frame layoutMode: "NONE" in pratica sono solo contenitori per un singolo figlio (nessuna sovrapposizione) e potrebbero essere semplificati. L'avviso lascia che l'agente decida come gestirlo.

absoluteBoundingBox — perché è preservato

Ogni nodo nell'IR mantiene il suo absoluteBoundingBox da Figma, anche quando il genitore è uno stack (dove le posizioni assolute sono teoricamente irrilevanti per il layout):

{
  "kind": "stack",
  "absoluteBoundingBox": { "x": 0, "y": 88, "width": 390, "height": 756 },
  ...
}

Questo è lì per gli agenti che hanno bisogno di ragionare sulle relazioni spaziali tra nodi che non condividono una relazione diretta genitore-figlio. La sovrapposizione di due elementi da rami diversi dell'albero richiede di conoscere le loro posizioni assolute, non solo i loro offset relativi. Il sistema di coordinate è il canvas di Figma — origine in alto a sinistra, y crescente verso il basso.

Aiuta anche con il riferimento incrociato PNG. Il file _meta.json include un campo pngCount e i PNG esportati sono nominati per slug della schermata. Se stai confrontando l'IR con lo screenshot, absoluteBoundingBox ti permette di localizzare nodi specifici all'interno dell'immagine. Per vedere questo sul tuo design, esporta un bundle da figmascope.dev.

Riempimenti e bordi sui tipi contenitore

Un punto di confusione comune: i nodi stack e overlay possono avere riempimenti, non solo i leaf. Una colonna con un colore di sfondo è comunque uno stack — ha solo anche un array fills. figmascope lo preserva:

{
  "kind": "stack",
  "axis": "vertical",
  "fills": [{ "type": "SOLID", "color": "#f6f2ea" }],
  "cornerRadius": 12,
  ...
}

In Compose, questo tipicamente diventa una Column all'interno di una Surface o Card, con il colore di riempimento e il corner radius applicati al contenitore. L'IR non collassa il riempimento nei figli — lo mantiene sul nodo dove Figma lo ha impostato.

I riempimenti a gradiente sui tipi contenitore emettono un avviso background-gradient-not-supported:<name>. Il nodo è comunque presente nell'IR con gli altri campi intatti. L'agente dovrebbe trattare il riempimento come un TODO e approssimare con un colore solido o generare una chiamata di disegno personalizzata, secondo le note di scope di CONTEXT.md.

La relazione stringRef + text

I nodi leaf di testo portano entrambi:

Se il contenuto di un nodo di testo è stato escluso da strings.json a causa di una collisione o di un filtro (solo numerico, vuoto, troppo corto), il leaf ha ancora text ma stringRef sarà assente. L'agente dovrebbe ricadere sul letterale in quel caso. Vedi strings.json per la logica completa di gestione delle collisioni.

Quando entrambi sono presenti, il vincolo di CONTEXT.md dice di usare stringRef. Il campo text è lì per il ragionamento dell'agente (in modo che sappia cosa dice la stringa senza aprire strings.json) e come fallback.

Istanze di componenti nell'IR

Quando un nodo Figma è un'INSTANCE di un componente, il nodo IR porta due campi aggiuntivi:

{
  "kind": "stack",
  "componentId": "789:012",
  "componentName": "PrimaryButton",
  ...
}

Questo si collega a components/inventory.json. L'agente sa che questo nodo è un'istanza di PrimaryButton piuttosto che un layout personalizzato, e dovrebbe fare riferimento al componente esistente invece di generare codice duplicato. La copertura completa di questo è in Inventario dei Componenti.

Esempio di schermata reale

Un esempio semplificato ma strutturalmente accurato di una schermata con un header, una colonna di contenuto e un pulsante flottante:

{
  "screen": "home",
  "root": {
    "kind": "overlay",
    "name": "HomeScreen",
    "width": 390,
    "height": 844,
    "fills": [{ "type": "SOLID", "color": "#0d0d1a" }],
    "children": [
      {
        "kind": "absolute",
        "offset": { "x": 0, "y": 0 },
        "child": {
          "kind": "stack",
          "name": "ContentArea",
          "axis": "vertical",
          "gap": 24,
          "paddingTop": 56,
          "paddingLeft": 20,
          "paddingRight": 20,
          "children": [
            {
              "kind": "leaf",
              "name": "Title",
              "text": "Speed Test",
              "stringRef": "speed.test",
              "fontSize": 28,
              "fontWeight": 700
            }
          ]
        }
      },
      {
        "kind": "absolute",
        "offset": { "x": 111, "y": 752 },
        "child": {
          "kind": "stack",
          "name": "StartButton",
          "componentId": "321:654",
          "componentName": "PrimaryButton",
          "axis": "horizontal",
          "gap": 8,
          "paddingTop": 16,
          "paddingBottom": 16,
          "paddingLeft": 32,
          "paddingRight": 32,
          "cornerRadius": 24,
          "fills": [{ "type": "SOLID", "color": "#7f5cfe" }]
        }
      }
    ]
  }
}

Da questo l'agente può generare correttamente una schermata Compose Box con una Column posizionata in modo assoluto per il contenuto e un componente PrimaryButton posizionato in modo assoluto in basso. Ogni decisione di layout è derivabile dall'IR senza indovinare.

Per come si applicano i token ai valori di spaziatura e colore in questa struttura, vedi tokens.json Spiegato. Per il flusso di lavoro completo dell'agente usando questo IR con Cursor o Claude Code, vedi Jetpack Compose da Figma. Provalo tu stesso incollando il tuo URL Figma in figmascope.