A árvore de nós do Figma é rica mas ruidosa. Frame, Group, Component, Instance, Text, Rectangle, Ellipse, Vector — cada um tem dezenas de campos opcionais, alguns conflitando entre si. Um agente trabalhando diretamente na resposta bruta da API do Figma gasta seu orçamento cognitivo entendendo o schema em vez de escrever código.

O figmascope normaliza a árvore em quatro tipos de nó: stack, overlay, absolute e leaf. Todo nó em todo arquivo screens/*.json é um desses quatro. Nada mais.

Os quatro tipos

stack

Um stack é um nó com Auto Layout do Figma — layoutMode: "VERTICAL" ou layoutMode: "HORIZONTAL". Seus filhos são posicionados em fluxo ao longo do eixo. Gaps e padding são explícitos.

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

Mapeia para Jetpack Compose como Column (eixo vertical) ou Row (eixo horizontal). Os campos primaryAxisAlignItems e counterAxisAlignItems mapeiam para Arrangement e Alignment respectivamente. gap se torna Arrangement.spacedBy().

overlay

Um overlay é um nó cujos filhos têm posições absolutas dentro dele. O Figma representa isso como um Frame com layoutMode: "NONE" onde os filhos têm coordenadas absoluteBoundingBox. O tipo overlay captura isso sem exigir que o agente raciocine sobre coordenadas brutas para entender a estrutura.

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

Mapeia para Compose como Box. Filhos são posicionados usando Modifier.offset() ou Modifier.align() dependendo de como suas posições se relacionam com os limites do pai.

absolute

Um nó absolute é um wrapper que carrega um único filho em um offset específico (x, y) dentro do seu overlay pai. É um nó estrutural fino — existe para preservar a relação espacial do Figma sem confundir posição com conteúdo.

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

No Compose, isso tipicamente se torna um filho de um Box com Modifier.offset(x.dp, y.dp). Os valores de offset são candidatos à substituição por token se existirem tokens de espaçamento no intervalo.

leaf

Um leaf é um nó terminal — sem filhos. Tem estilização (fills, strokes, corner radius) e opcionalmente conteúdo de texto. Leaves de texto têm um campo text (a string literal) e um campo stringRef (a chave de recurso i18n de strings.json).

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

Mapeia para Compose como um composable Text() para conteúdo de texto, ou como Surface, Box ou Image para leaves não-texto dependendo do tipo de fill.

Os quatro tipos mapeiam um-para-um com o modelo composicional de qualquer framework de UI. Stacks são layouts de fluxo. Overlays são containers posicionados em z. Absolutes carregam metadados espaciais. Leaves são conteúdo. É isso. A taxonomia de nós do Figma tem 12+ tipos de nó — o IR reduz para quatro.

Como o tipo é determinado

A lógica de classificação:

  1. Se layoutMode é "VERTICAL" ou "HORIZONTAL"stack
  2. Se layoutMode é "NONE" e o nó tem filhos → overlay (filhos embrulhados como absolute)
  3. Se um nó é filho direto de um overlay e carrega uma posição → absolute
  4. Se o nó não tem filhos e não é um wrapper absolute → leaf

O aviso layout-mode-none-inferred em _meta.json dispara quando um Frame com layoutMode: "NONE" tem filhos com bounding boxes não-trivialmente sobrepostas. O figmascope o trata como overlay, mas anota a inferência porque alguns frames com layoutMode: "NONE" na prática são apenas containers para um único filho (sem sobreposição) e poderiam ser simplificados. O aviso deixa o agente decidir como lidar com isso.

absoluteBoundingBox — por que é preservado

Todo nó no IR retém seu absoluteBoundingBox do Figma, mesmo quando o pai é um stack (onde posições absolutas são teoricamente irrelevantes para o layout):

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

Isso existe para agentes que precisam raciocinar sobre relações espaciais entre nós que não compartilham uma relação direta pai-filho. Sobrepor dois elementos de ramos diferentes da árvore requer conhecer suas posições absolutas, não apenas seus offsets relativos. O sistema de coordenadas é o canvas do Figma — origem no canto superior esquerdo, y crescendo para baixo.

Também ajuda com referência cruzada de PNG. O _meta.json inclui um campo pngCount e os PNGs exportados são nomeados pelo slug da tela. Se você está comparando o IR com o screenshot, absoluteBoundingBox permite localizar nós específicos dentro da imagem. Para ver isso no seu próprio design, exporte um bundle no figmascope.dev.

Fills e strokes em tipos container

Um ponto comum de confusão: nós stack e overlay podem ter fills, não apenas leaves. Uma coluna com cor de fundo ainda é um stack — ela apenas também tem um array fills. O figmascope preserva isso:

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

No Compose, isso tipicamente se torna um Column dentro de uma Surface ou Card, com a cor de fill e o corner radius aplicados ao container. O IR não colapsa o fill nos filhos — ele o mantém no nó onde o Figma o definiu.

Fills com gradiente em tipos container emitem um aviso background-gradient-not-supported:<name>. O nó ainda está presente no IR com seus outros campos intactos. O agente deve tratar o fill como um TODO e either aproximar com uma cor sólida ou gerar uma chamada de desenho customizado, conforme as notas de escopo do CONTEXT.md.

A relação stringRef + text

Nós leaf de texto carregam ambos:

Se o conteúdo de um nó de texto foi removido de strings.json devido a colisão ou filtro (numérico-apenas, vazio, muito curto), o leaf ainda tem text mas stringRef estará ausente. O agente deve recorrer ao literal nesse caso. Veja strings.json para a lógica completa de tratamento de colisão.

Quando ambos estão presentes, a restrição do CONTEXT.md diz para usar stringRef. O campo text está lá para raciocínio do agente (para que ele saiba o que a string diz sem abrir strings.json) e como fallback.

Instâncias de componente no IR

Quando um nó do Figma é uma INSTANCE de um componente, o nó do IR carrega dois campos adicionais:

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

Isso vincula de volta a components/inventory.json. O agente sabe que esse nó é uma instância de PrimaryButton em vez de um layout específico, e deve referenciar o componente existente em vez de gerar código duplicado. A cobertura completa disso está em Inventário de Componentes.

Exemplo real de tela

Um exemplo simplificado mas estruturalmente correto de uma tela com cabeçalho, coluna de conteúdo e botão flutuante:

{
  "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": "Teste de Velocidade",
              "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" }]
        }
      }
    ]
  }
}

A partir disso, o agente pode gerar corretamente uma tela Compose Box com um Column posicionado absolutamente para o conteúdo e um componente PrimaryButton posicionado absolutamente na parte inferior. Cada decisão de layout é derivável do IR sem adivinhação.

Para como os tokens se aplicam aos valores de espaçamento e cor nessa estrutura, veja tokens.json Explicado. Para o fluxo completo de agente usando esse IR com Cursor ou Claude Code, veja Jetpack Compose a partir do Figma. Experimente você mesmo colando sua URL do Figma no figmascope.