El árbol de nodos de Figma es rico pero ruidoso. Frame, Group, Component, Instance, Text, Rectangle, Ellipse, Vector — cada uno tiene docenas de campos opcionales, algunos de los cuales entran en conflicto entre sí. Un agente que trabaja directamente con la respuesta raw de la API de Figma gasta presupuesto cognitivo entendiendo el esquema en lugar de escribir código.

figmascope normaliza el árbol en cuatro tipos de nodos: stack, overlay, absolute y leaf. Cada nodo en cada archivo screens/*.json es uno de estos cuatro. Nada más.

Los cuatro tipos

stack

Un stack es un nodo con Auto Layout de Figma — layoutMode: "VERTICAL" o layoutMode: "HORIZONTAL". Sus hijos están posicionados en flujo a lo largo del eje. Los espacios y el padding son 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": [ ... ]
}

Se mapea a Jetpack Compose como un Column (eje vertical) o Row (eje horizontal). Los campos primaryAxisAlignItems y counterAxisAlignItems se mapean a Arrangement y Alignment respectivamente. gap se convierte en Arrangement.spacedBy().

overlay

Un overlay es un nodo cuyos hijos tienen posiciones absolutas dentro de él. Figma lo representa como un Frame con layoutMode: "NONE" donde los hijos tienen coordenadas absoluteBoundingBox. El tipo overlay captura esto sin requerir que el agente razone sobre coordenadas raw para entender la estructura.

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

Se mapea a Compose como un Box. Los hijos se posicionan usando Modifier.offset() o Modifier.align() dependiendo de cómo sus posiciones se relacionan con los límites del padre.

absolute

Un nodo absolute es un contenedor que lleva un único hijo en un desplazamiento específico (x, y) dentro de su overlay padre. Es un nodo estructural delgado — existe para preservar la relación espacial de Figma sin confundir posición con contenido.

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

En Compose, esto típicamente se convierte en un hijo de un Box con Modifier.offset(x.dp, y.dp). Los valores de desplazamiento son candidatos para sustitución de tokens si existen tokens de espaciado en el rango.

leaf

Un leaf es un nodo terminal — sin hijos. Tiene estilo (fills, strokes, radio de esquinas) y opcionalmente contenido de texto. Los leaves de texto tienen un campo text (la cadena literal) y un campo stringRef (la clave de recurso i18n de 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
}

Se mapea a Compose como un Composable Text() para contenido de texto, o como un Surface, Box o Image para leaves sin texto dependiendo del tipo de fill.

Los cuatro tipos se mapean uno a uno al modelo composicional de cualquier framework de UI. Los stacks son layouts de flujo. Los overlays son contenedores posicionados en z. Los absolutes llevan metadatos espaciales. Los leaves son contenido. Eso es todo. La taxonomía de nodos de Figma tiene más de 12 tipos de nodos — el IR lo reduce a cuatro.

Cómo se determina el tipo

La lógica de clasificación:

  1. Si layoutMode es "VERTICAL" o "HORIZONTAL"stack
  2. Si layoutMode es "NONE" y el nodo tiene hijos → overlay (hijos envueltos como absolute)
  3. Si un nodo es el hijo directo de un overlay y lleva una posición → absolute
  4. Si el nodo no tiene hijos y no es un contenedor absolute → leaf

La advertencia layout-mode-none-inferred en _meta.json se activa cuando un Frame con layoutMode: "NONE" tiene hijos con cajas delimitadoras que se superponen de forma no trivial. figmascope lo trata como un overlay, pero anota la inferencia porque algunos frames con layoutMode: "NONE" en la práctica son solo contenedores para un único hijo (sin superposición) y podrían simplificarse. La advertencia deja que el agente decida cómo manejarlo.

absoluteBoundingBox — por qué se preserva

Cada nodo en el IR retiene su absoluteBoundingBox de Figma, incluso cuando el padre es un stack (donde las posiciones absolutas son teóricamente irrelevantes para el layout):

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

Esto está ahí para los agentes que necesitan razonar sobre las relaciones espaciales entre nodos que no comparten una relación padre-hijo directa. Superponer dos elementos de ramas diferentes del árbol requiere conocer sus posiciones absolutas, no solo sus desplazamientos relativos. El sistema de coordenadas es el lienzo de Figma — origen en la esquina superior izquierda, y aumentando hacia abajo.

También ayuda con la referencia cruzada de PNG. El _meta.json incluye un campo pngCount y los PNGs exportados llevan el nombre del slug de la pantalla. Si estás comparando el IR con la captura de pantalla, absoluteBoundingBox te permite localizar nodos específicos dentro de la imagen. Para ver esto en tu propio diseño, exporta un bundle desde figmascope.dev.

Fills y strokes en los tipos contenedor

Un punto de confusión común: los nodos stack y overlay pueden tener fills, no solo los leaves. Una columna con color de fondo sigue siendo un stack — simplemente también tiene un array fills. figmascope lo preserva:

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

En Compose, esto típicamente se convierte en un Column dentro de un Surface o Card, con el color de fill y el radio de esquinas aplicados al contenedor. El IR no colapsa el fill en los hijos — lo mantiene en el nodo donde Figma lo estableció.

Los fills de degradado en tipos contenedor emiten una advertencia background-gradient-not-supported:<name>. El nodo sigue presente en el IR con el resto de sus campos intactos. El agente debe tratar el fill como un TODO y aproximar con un color sólido o generar una llamada de dibujo personalizada, según las notas de alcance del CONTEXT.md.

La relación stringRef + text

Los nodos leaf de texto llevan ambos:

Si el contenido de un nodo de texto fue descartado de strings.json debido a una colisión o filtro (solo numérico, vacío, demasiado corto), el leaf sigue teniendo text pero stringRef estará ausente. El agente debe recurrir al literal en ese caso. Consulta strings.json para la lógica completa de manejo de colisiones.

Cuando ambos están presentes, la restricción de CONTEXT.md dice usar stringRef. El campo text está ahí para el razonamiento del agente (para que sepa qué dice la cadena sin abrir strings.json) y como fallback.

Instancias de componentes en el IR

Cuando un nodo de Figma es una INSTANCE de un componente, el nodo IR lleva dos campos adicionales:

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

Esto se enlaza de vuelta a components/inventory.json. El agente sabe que este nodo es una instancia de PrimaryButton en lugar de un layout específico, y debe referenciar el componente existente en lugar de generar código duplicado. La cobertura completa de esto está en Component Inventory.

Ejemplo real de pantalla

Un ejemplo simplificado pero estructuralmente preciso de una pantalla con un encabezado, columna de contenido y botón flotante:

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

A partir de esto, el agente puede generar correctamente una pantalla Box de Compose con un Column posicionado absolutamente para el contenido y un componente PrimaryButton posicionado absolutamente en la parte inferior. Cada decisión de layout es derivable del IR sin adivinar.

Para cómo se aplican los tokens a los valores de espaciado y color en esta estructura, consulta tokens.json Explicado. Para el flujo de trabajo completo del agente usando este IR con Cursor o Claude Code, consulta Jetpack Compose desde Figma. Pruébalo tú mismo pegando tu URL de Figma en figmascope.