EduFlow

الوحدة 2 · الـ Design Tokens

خطوط إنتاج الـ Tokens: من JSON إلى كل منصّة

20 دقراءة 4 داملأ الفراغات

الدرس في لمحة

جهّز خط إنتاج (pipeline) لبناء الـ tokens يحوّل JSON بصيغة DTCG إلى CSS custom properties لكل theme وإلى صيغ أصلية لكل منصّة، مع الحفاظ على المراجع حتى يبقى تغيير الـ theme وقت التشغيل ممكنًا.

  1. 1ماذا يفعل خط الإنتاج
  2. 2احتفظ بالمراجع
  3. 3خط إنتاج حقيقي مع Style Dictionary
  4. 4فحوص تعمل مع البناء

لا فيديو هنا، بل الشيء الحقيقي: درس مكتوب وأمثلة قابلة للتشغيل واختبار وتدريب.

بعد أن صارت الـ tokens مخزّنة كـ JSON بصيغة DTCG، يجب أن يحوّلها شيء ما إلى ما تستهلكه كل منصّة: CSS custom properties لتطبيق الويب والموقع التسويقي، وSwift لتطبيق السائقين على iOS، وKotlin أو XML لـ Android. هذا الشيء هو خط إنتاج الـ tokens (token pipeline). إنه قطعة كود صغيرة ينتهي كل فريق نظام تصميم بامتلاكها، وإتقانها مبكرًا يوفّر شهورًا من الانحراف لاحقًا.

ماذا يفعل خط الإنتاج

ملفات JSON الخاصة بالـ tokens تمرّ بخطوات التحليل وحلّ المراجع والتحويل والتنسيق، فتنتج CSS للويب وSwift لـ iOS وKotlin لـ Android. *.tokens .json تحليل حلّ المراجع تحويل تنسيق CSS Swift Kotlin
خط إنتاج الـ tokens: مصدر واحد، وإخراج واحد لكل منصّة
  1. التحليل (parse): يقرأ كل ملفات المصدر ويدمجها دمجًا عميقًا في شجرة واحدة.
  2. حلّ المراجع (resolve): يحلّ الأسماء المستعارة مثل {color.blue.600}، ويرفض المراجع المكسورة أو الدائرية.
  3. التحويل (transform): يحوّل الأسماء والقيم لكل منصّة: يصبح color.action.bg هو --nw-color-action-bg في CSS وcolorActionBg في Swift؛ ويصبح 1rem هو 16 نقطة على iOS.
  4. التنسيق (format): يكتب النتيجة في ملف: كتلة custom properties، أو enum في Swift، أو ملف موارد Android.

هذا قلب جانب CSS، وهو صغير بما يكفي لتقرأه دفعة واحدة:

JavaScript
const tokens = {
  color: {
    $type: 'color',
    blue: { 600: { $value: { colorSpace: 'srgb', components: [0.122, 0.353, 0.878], hex: '#1f5ae0' } } },
    white: { $value: { colorSpace: 'srgb', components: [1, 1, 1], hex: '#ffffff' } },
    action: {
      bg: { $value: '{color.blue.600}' },
      text: { $value: '{color.white}' },
    },
  },
  space: { $type: 'dimension', 4: { $value: { value: 1, unit: 'rem' } } },
};

// Walk the tree and collect every token with its path.
function flatten(node, path = [], type) {
  const t = node.$type ?? type;
  if ('$value' in node) return [{ path, type: t, value: node.$value }];
  return Object.entries(node)
    .filter(([key]) => !key.startsWith('
    
  

))
    .flatMap(([key, child]) => flatten(child, [...path, key], t));
}

const cssName = (path) => `--nw-${path.join('-')}`;
function cssValue({ type, value }) {
  if (typeof value === 'string' && value.startsWith('{')) {
    return `var(${cssName(value.slice(1, -1).split('.'))})`; // keep the reference
  }
  if (type === 'color') return value.hex;
  if (type === 'dimension') return `${value.value}${value.unit}`;
  return String(value);
}

const lines = flatten(tokens).map((t) => `  ${cssName(t.path)}: ${cssValue(t)};`);
console.log(`/* Generated from tokens. Do not edit. */\n:root {\n${lines.join('\n')}\n}`);

انظر إلى الإخراج. المسار يصبح الاسم، فيخرج color.blue.600 بالشكل --nw-color-blue-600. في الدروس السابقة اختصرنا الـ primitives إلى --nw-blue-600 لتسهيل القراءة، لكن في نظام مولَّد يكون الاسم هو ما تقوله بنية المجموعات، وهذا سبب إضافي لتصمّم المجموعات والأسماء معًا. والأهم أن الـ semantic tokens تخرج بالشكل var(--nw-color-blue-600) لا كقيمة hex. وهذا الاختيار مهمّ بما يكفي ليستحقّ قسمًا خاصًا به.

احتفظ بالمراجع

يمكن لخط الإنتاج إما أن يحلّ الأسماء المستعارة إلى قيمها النهائية، أو أن يُبقيها مراجع في الإخراج. في CSS، أبقِها. الإخراج المحلول يسطّح طبقاتك: يرى المتصفّح --nw-color-action-bg: #1f5ae0، فلا يفعل تخصيص الـ primitive وقت التشغيل شيئًا، ولا يستطيع مطوّر يفحص زرًا في DevTools أن يرى من أي primitive جاء لونه. أما الإخراج بالمراجع فيحفظ البنية التي صمّمتها بالضبط، بما فيها تغيير الـ theme على الأشجار الفرعية من درس الـ themes.

المنصّات الأصلية مختلفة. الثوابت في Swift وKotlin تُحلّ عادةً إلى قيمها النهائية، لأن هذه المنصّات تتعامل مع الـ themes بآلياتها الخاصة (asset catalogs، وcolor schemes في Compose).

خط إنتاج حقيقي مع Style Dictionary

يمكنك أن تطوّر السكربت أعلاه إلى أداة كاملة، لكن أغلب الفرق تستخدم Style Dictionary، وهو أكثر أنظمة بناء الـ tokens مفتوحة المصدر انتشارًا. إصداراته الحديثة تقرأ ملفات DTCG مباشرة وتكتشف الصيغة تلقائيًا. وإعداد على طريقة Northwind يبني الـ theme الفاتح داخل :root والـ theme الداكن تحت الـ attribute selector الخاص به:

build-tokens.mjs
import StyleDictionary from 'style-dictionary';

const themes = {
  light: ':root',
  dark: '[data-theme="dark"]',
};

for (const [name, selector] of Object.entries(themes)) {
  const sd = new StyleDictionary({
    include: ['tokens/primitives.tokens.json'],
    source: [`tokens/semantic-${name}.tokens.json`],
    platforms: {
      css: {
        transformGroup: 'css',
        prefix: 'nw',
        buildPath: 'dist/css/',
        files: [{
          destination: `theme-${name}.css`,
          format: 'css/variables',
          filter: name === 'light' ? undefined : (token) => token.isSource,
          options: { selector, outputReferences: true },
        }],
      },
    },
  });
  await sd.buildAllPlatforms();
}

الأفكار الأساسية: الـ primitives تدخل عبر include حتى يمكن حلّ المراجع، بينما يدخل الملف الدلالي الخاص بالـ theme عبر source. وفي الـ theme الداكن يُبقي الـ filter الـ tokens المصدرية وحدها، فيحتوي الملف الداكن على الـ semantic tokens المعاد ربطها لا على نسخة ثانية من كل primitive. وoutputReferences: true يُبقي مراجع var() في الإخراج. وقد يحذّرك Style Dictionary من أن الملف الداكن يشير إلى tokens استبعدها الـ filter؛ وهذا مقصود هنا، لأن الملف الفاتح يعرّف الـ primitives على :root أصلًا.

فحوص تعمل مع البناء

حين تصبح الـ tokens بيانات، يستطيع الـ CI أن يختبر قرارات التصميم كما يختبر الكود. يشغّل خط الإنتاج في Northwind ثلاثة فحوص على كل pull request: فحص التسمية (lint) الذي رأيته سابقًا، وفحص مراجع يفشل عند الأسماء المستعارة المكسورة أو الدائرية، وفحص تباين يحلّ كل زوج موثّق من النص والخلفية في كل theme ويفشل تحت 4.5:1. وهذا الأخير التقط تراجعًا في الوضع الداكن لعلامة Tidewater قبل أن يراه أي مصمّم.

في التمرين تكمل إعداد بناء الـ theme الداكن. وبهذا تكتمل معمارية الـ tokens؛ والقسم التالي يبني المكوّنات فوقها.

أهم النقاط

  • خط إنتاج الـ tokens يحلّل ملفات المصدر، ويحلّ الأسماء المستعارة، ويحوّل الأسماء والقيم لكل منصّة، ويكتب صيغة إخراج واحدة لكل منصّة.
  • احتفظ بالمراجع في إخراج CSS (outputReferences) حتى تبقى الـ semantic tokens تشير إلى الـ primitives وقت التشغيل، ويستمرّ تغيير الـ theme على الأشجار الفرعية في العمل.
  • ابنِ كل theme من الـ primitives المشتركة مع الملف الدلالي الخاص بذلك الـ theme، وأخرج الـ tokens الخاصة بالـ theme وحدها تحت الـ selector الخاص به.
  • الملفات المولَّدة مخرجات بناء؛ لا يعدّلها أحد يدويًا، والـ CI يعيد بناءها مع كل تغيير.

قراءات إضافية

تحقّق من فهمك

3 أسئلة

السؤال 1 من 3

يكتب خط الإنتاج --nw-color-action-bg: #1f5ae0; بدل --nw-color-action-bg: var(--nw-color-blue-600);. ما الذي ينكسر؟

السؤال 2 من 3

لماذا يستخدم بناء الـ theme الداكن include للـ primitives وsource للملف الدلالي الداكن، ثم يصفّي على isSource؟

السؤال 3 من 3

عدّل مهندس منتج الملف dist/css/theme-dark.css مباشرة ليصلح خللًا في التباين، فاختفى الإصلاح بعد الإصدار التالي. ماذا كان يجب أن تكون العملية؟

تدريب

املأ الفراغات

أكمل بناء الـ theme الداكن

املأ إعداد Style Dictionary الذي يولّد الـ theme الداكن في Northwind. يجب أن يحقّق الإخراج ما يلي:

  • أن يُسمّى كل متغيّر بالبادئة nw (--nw-…)،
  • أن يستخدم صيغة CSS custom properties المدمجة في Style Dictionary،
  • أن يحتوي الـ semantic tokens الداكنة وحدها، دون الـ primitives المحمّلة عبر include،
  • أن يضعها داخل الـ attribute selector الذي يحوّل أي عنصر إلى الداكن، مكتوبًا بعلامات تنصيص مزدوجة داخل النص المحاط بعلامات مفردة،
  • أن يُبقي مراجع var() بدل القيم المحلولة.
export default { include: ['tokens/primitives.tokens.json'], source: ['tokens/semantic-dark.tokens.json'], platforms: { css: { transformGroup: 'css', prefix: '', buildPath: 'dist/css/', files: [{ destination: 'theme-dark.css', format: '', filter: (token) => token., options: { selector: '', outputReferences: , }, }], }, }, };