شجرة عقد Figma غنية لكنها صاخبة. Frame وGroup وComponent وInstance وText وRectangle وEllipse وVector — كل منها لديها عشرات الحقول الاختيارية، بعضها يتعارض مع بعض. وكيل يعمل مباشرة على استجابة Figma API الخام يُنفق ميزانيته المعرفية في فهم المخطط بدلاً من كتابة الكود.

يُطبّع figmascope الشجرة إلى أربعة أنواع عقد: stack وoverlay وabsolute وleaf. كل عقدة في كل ملف screens/*.json هي واحدة من هذه الأربعة. لا شيء غيرها.

الأنواع الأربعة

stack

المكدّس هو عقدة بـ Figma Auto Layout — 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 لعقد الأوراق غير النصية بحسب نوع الملء.

الأنواع الأربعة تُعيَّن واحداً إلى واحد إلى النموذج التركيبي لأي إطار عمل واجهة مستخدم. المكدّسات تخطيطات تدفق. الـ 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" أطفال لديهم صناديق حدودية متداخلة بشكل غير تافه. figmascope يعامله كـ overlay، لكنه يُلاحظ الاستنتاج لأن بعض إطارات layoutMode: "NONE" في الممارسة هي مجرد حاويات لطفل واحد (صفر تداخل) ويمكن تبسيطها. التحذير يُتيح للوكيل تقرير كيفية التعامل معه.

absoluteBoundingBox — لماذا يُحفَظ

كل عقدة في IR تحتفظ بـ absoluteBoundingBox من Figma، حتى عندما يكون الأصل مكدّساً (حيث المواضع المطلقة غير ذات صلة نظرياً بالتخطيط):

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

هذا موجود للوكلاء الذين يحتاجون إلى التفكير في العلاقات المكانية عبر عقد لا تشترك في علاقة أصل-طفل مباشرة. تداخل عنصرين من فروع مختلفة في الشجرة يتطلب معرفة مواضعهما المطلقة، لا مجرد إزاحاتهما النسبية. نظام الإحداثيات لقماش Figma — أصل في أعلى اليسار، y يتزايد للأسفل.

يساعد أيضاً في الإسناد المرجعي لـ PNG. يتضمن _meta.json حقل pngCount والـ PNGs المصدَّرة مُسمّاة بشريحة الشاشة. إذا كنت تقارن IR بلقطة الشاشة، يُتيح لك absoluteBoundingBox تحديد موقع عقد محددة داخل الصورة. لرؤية هذا على تصميمك، صدّر حزمة من figmascope.dev.

الملءات والحدود على أنواع الحاويات

نقطة ارتباك شائعة: عقد stack وoverlay يمكنها أن تحتوي ملءات، ليس فقط الـ leaves. عمود بلون خلفية لا يزال مكدّساً — له أيضاً مصفوفة 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

عقد leaf النصية تحمل كليهما:

إذا حُذف محتوى عقدة نص من strings.json بسبب تصادم أو فلتر (رقمي فقط، فارغ، قصير جداً)، تظل للـ leaf قيمة 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. جرّبه بنفسك من خلال لصق رابط Figma في figmascope.