Дерево вузлів Figma — багате, але галасливе. Frame, Group, Component, Instance, Text, Rectangle, Ellipse, Vector — кожен має десятки необов'язкових полів, деякі з яких суперечать одне одному. Агент, що працює безпосередньо з сирою відповіддю API Figma, витрачає когнітивний бюджет на розуміння схеми замість написання коду.

figmascope нормалізує дерево до чотирьох типів вузлів: stack, overlay, absolute та leaf. Кожен вузол у кожному файлі screens/*.json — один із цих чотирьох. Нічого більше.

Чотири типи

stack

Stack — це вузол із Auto Layout Figma — layoutMode: "VERTICAL" або layoutMode: "HORIZONTAL". Його дочірні елементи розміщені в потоці вздовж осі. Відступи між елементами та внутрішні відступи є явними.

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

Відображається у Jetpack Compose як Column (вертикальна вісь) або Row (горизонтальна вісь). Поля primaryAxisAlignItems та counterAxisAlignItems відповідно відображаються на Arrangement та Alignment. gap стає Arrangement.spacedBy().

overlay

Overlay — це вузол, дочірні елементи якого мають абсолютні позиції всередині нього. Figma представляє це як Frame із layoutMode: "NONE", де дочірні елементи мають координати absoluteBoundingBox. Тип overlay фіксує це без необхідності для агента міркувати про сирі координати для розуміння структури.

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

Відображається у Compose як Box. Дочірні елементи позиціонуються за допомогою Modifier.offset() або Modifier.align() залежно від того, як їх позиції співвідносяться з межами батьківського вузла.

absolute

Absolute — це вузол-обгортка, що несе єдиного нащадка у конкретному зміщенні (x, y) всередині батьківського overlay. Це тонкий структурний вузол — він існує для збереження просторових зв'язків із Figma без змішування позиції та вмісту.

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

У Compose це зазвичай стає дочірнім елементом Box із Modifier.offset(x.dp, y.dp). Значення зміщення є кандидатами для підстановки токенів, якщо існують токени відступів у відповідному діапазоні.

leaf

Leaf — термінальний вузол без дочірніх елементів. Він має стилізацію (заливки, обводки, радіус кутів) і необов'язково текстовий вміст. Текстові листи мають поле text (літеральний рядок) та поле stringRef (ключ i18n-ресурсу з 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
}

Відображається у Compose як composable Text() для текстового вмісту або як Surface, Box чи Image для нетекстових листів залежно від типу заливки.

Чотири типи відображаються один до одного на композиційну модель будь-якого UI-фреймворку. Stacks — потокові розкладки. Overlays — контейнери з z-позиціонуванням. Absolutes — носії просторових метаданих. Leaves — вміст. Це все. Таксономія вузлів Figma має 12+ типів — IR зводить їх до чотирьох.

Як визначається тип

Логіка класифікації:

  1. Якщо layoutMode"VERTICAL" або "HORIZONTAL"stack
  2. Якщо layoutMode"NONE" і вузол має дочірні елементи → overlay (дочірні обгортаються як absolute)
  3. Якщо вузол є прямим дочірнім overlay і несе позицію → absolute
  4. Якщо вузол не має дочірніх і не є обгорткою absolute → leaf

Попередження layout-mode-none-inferred у _meta.json спрацьовує, коли Frame із layoutMode: "NONE" має дочірні елементи з нетривіально перекриваючими bounding box. figmascope обробляє його як overlay, але помічає виведення, оскільки деякі фрейми layoutMode: "NONE" на практиці є просто контейнерами для єдиного нащадка (нульове перекриття) і могли б бути спрощені. Попередження дозволяє агенту вирішити, як його обробити.

absoluteBoundingBox — чому він збережений

Кожен вузол у IR зберігає свій absoluteBoundingBox із Figma, навіть коли батьківський — stack (де абсолютні позиції теоретично нерелевантні для розкладки):

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

Це потрібно агентам, яким необхідно міркувати про просторові зв'язки між вузлами, що не мають прямого зв'язку батько-нащадок. Накладання двох елементів із різних гілок дерева вимагає знання їх абсолютних позицій, а не лише відносних зміщень. Система координат — полотно Figma: початок у верхньому лівому куті, y збільшується донизу.

Це також допомагає при перехресному посиланні з PNG. _meta.json містить поле pngCount, а експортовані PNG названі за slug екрана. При порівнянні IR зі скриншотом absoluteBoundingBox дозволяє знайти конкретні вузли на зображенні. Щоб побачити це на власному дизайні — експортуйте бандл із figmascope.dev.

Заливки та обводки на контейнерних типах

Поширена точка плутанини: вузли stack та overlay можуть мати заливки, не лише листи. Колонка з кольором фону — все одно stack, що просто також має масив fills. figmascope зберігає це:

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

У Compose це зазвичай стає Column всередині Surface або Card із кольором заливки та радіусом кутів, застосованими до контейнера. IR не переносить заливку на дочірні елементи — він залишає її на вузлі, де Figma її встановила.

Градієнтні заливки на контейнерних типах генерують попередження background-gradient-not-supported:<name>. Вузол все одно присутній у IR з іншими своїми полями. Агент повинен обробити заливку як TODO і або наблизити суцільним кольором, або генерувати кастомний виклик малювання відповідно до нотаток про скоуп у CONTEXT.md.

Зв'язок stringRef та text

Текстові вузли-листи несуть обидва:

Якщо вміст текстового вузла було вилучено з strings.json через колізію або фільтр (лише цифри, порожній, надто короткий) — лист все одно має text, але stringRef буде відсутнім. Агент повинен у такому випадку повернутися до літерального значення. Повну логіку обробки колізій дивіться у strings.json.

Коли обидва присутні, обмеження CONTEXT.md каже використовувати stringRef. Поле text — для міркувань агента (щоб він знав, що говорить рядок, не відкриваючи strings.json) і як резервне значення.

Інстанції компонентів у IR

Коли вузол Figma є INSTANCE компонента, вузол IR несе два додаткові поля:

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

Це посилається на components/inventory.json. Агент знає, що цей вузол є інстанцією PrimaryButton, а не індивідуальною розкладкою, і повинен посилатися на існуючий компонент замість генерації дублювального коду. Повне розкриття цього — у інвентарі компонентів.

Реальний приклад екрана

Спрощений, але структурно точний приклад екрана із заголовком, колонкою вмісту та плаваючою кнопкою:

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

З цього агент може правильно генерувати екран Compose Box із абсолютно-позиціонованою Column для вмісту та абсолютно-позиціонованим компонентом PrimaryButton знизу. Кожне рішення щодо розкладки виводиться з IR без здогадок.

Для розуміння того, як токени застосовуються до значень відступів та кольорів у цій структурі, дивіться tokens.json пояснено. Для повного воркфлоу агента з використанням цього IR у Cursor або Claude Code — дивіться Jetpack Compose з Figma. Спробуйте самостійно, вставивши URL Figma у figmascope.