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:
- Se
layoutModeé"VERTICAL"ou"HORIZONTAL"→stack - Se
layoutModeé"NONE"e o nó tem filhos →overlay(filhos embrulhados comoabsolute) - Se um nó é filho direto de um overlay e carrega uma posição →
absolute - 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:
text: o valor de string literal do Figma (ex.:"Teste de Velocidade")stringRef: a chave de recurso destrings.json(ex.:"speed.test")
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.