PyDA Course
مبتدئ موجّه ⏱ 40 دقيقة ⚡ +100 XP +15 نقطة لكل خطوة

مولّد نظام التصميم

designcsscolor-theorydata-visualizationmatplotlib

كل نظام تصميم يبدأ من النقطة نفسها: يختار شخصٌ ما لون علامة تجارية واحدة ثم يسأل: «كيف ستبدو اللوحة كاملة؟». هذا المشروع يبني الإجابة في بايثون ، ستشتقّ لوحة ألوان كاملة في عائلة لونية واحدة، وتتحقق من كل زوج سترغب فعلًا في استخدامه وفق قواعد التباين في WCAG، وتبني مقياسًا للطباعة والمسافات يبقى متسقًا رياضيًا، وتُصدّر كل شيء كمتغيرات CSS جاهزة لإسقاطها في ورقة أنماط حقيقية.

يفترض هذا ما يعادل بايثون 101 وفهمًا تقريبيًا لشكل سلسلة ألوان hex ، لا شيء من مقرر تحليل البيانات مطلوب. هذا المشروع اختياري وغير مقيَّم؛ راجع المشاريع الواقعية للقائمة الكاملة المتزايدة.

🎯 ما ستفعله

  1. التحويل بين صيغ hex وRGB وHSL لتتم كل عملية على الألوان في الفضاء المناسب.
  2. اشتقاق لوحة كاملة ومتماسكة ، الدرجات والصبغات والرماديات ، من لون hex واحد للعلامة التجارية.
  3. تقييم أزواج النص/الخلفية وفق قواعد تباين WCAG حتى لا تكون إمكانية الوصول تخمينًا.
  4. بناء مقياس للطباعة بنسبة معيارية منتظمة ومقياس مسافات.
  5. إنشاء بطاقة عيّنة (سواتش) للوحة وحفظها كصورة قابلة للمشاركة.
  6. تصدير كل رمز كمتغيرات CSS مخصصة يمكنك لصقها في أي مشروع ويب.

أين تُشغّل هذا

محليًا مع uv هو المسار الأساسي الموصى به ، كل الحسابات بايثون نقي، ويقع ملفّا الإخراج (palette.png وdesign-tokens.css) مباشرةً في مجلد عملك.

GitHub Codespaces يعمل أيضًا بشكل ممتاز: افتح مستودع المقرر في Codespace مجاني وشغّله من هناك. كل خطوة تتصرف تمامًا كما في العمل المحلي.

Google Colab وKaggle Notebooks مناسبان جدًا لهذا المشروع ، لا تبعيات نظام، ولا ملفات على القرص، وكل شيء يظهر ضمنيًا. إذا كنت تتابع المقرر من دون إعداد محلي، فهذا أحد المشاريع الملائمة لنموذج الدفتر بنظافة.

Open In Colab Open In Kaggle Binder

الإعداد

كل ما تحتاجه موجود في المكتبة القياسية لبايثون إضافةً إلى مكتبة تصوير واحدة.

ثبّت uv

uv أداة واحدة تحل محل السلسلة المعتادة «ثبّت بايثون، ثم ثبّت pip، ثم ثبّت أداة بيئة افتراضية، ثم ثبّت الحزم» ، فهو يستطيع تثبيت إصدارات بايثون وإدارتها بنفسه، إلى جانب تبعيات مشروعك.

macOS / Linux (الطرفية):

curl -LsSf https://astral.sh/uv/install.sh | sh

Windows (PowerShell):

powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"

أغلق الطرفية وأعد فتحها، ثم تأكّد من التثبيت:

uv --version

أجهّز المشروع

uv init design-system
cd design-system
uv add matplotlib

تأتي colorsys مع بايثون وتتولى حسابات فضاء الألوان في الخطوة 1؛ وترسم matplotlib بطاقة سواتش اللوحة في الخطوة 5. تحتاج حزمة PyPI واحدة فقط.

✅ قائمة التحقق

  • ✅ يطبع uv --version رقم إصدار.
  • ✅ يوجد مجلد design-system/ بملف pyproject.toml، وmatplotlib مثبتة.

الخطوة 1: التحويل بين صيغ الألوان

لا يمكنك مزج الإضاءة وRGB: HSL هو الفضاء الذي يعدّل فيه البشر السطوع، وRGB هو الذي توصّل به الشاشات اللون، وhex هو ما تسمّي به الألوان في CSS. قبل أي عمل على اللوحة، ابنِ المترجمات الأربعة التي ستعتمد عليها لبقية المشروع.

1.1 اكتب دوال التحويل الأربع

👟 تلميح البداية : اعمل حول دالة بايثون colorsys.rgb_to_hls (وليست HSV): ترتيب ما تُرجعه (hue, lightness, saturation)، أي أنها تتبادل التشبّع والإضاءة مقارنةً بكل واجهة أخرى ستقابلها.

# design_system.py
import colorsys

def hex_to_rgb(hex_color: str) -> tuple[int, int, int]:
    """Convert a hex color string to an RGB tuple (0–255 per channel)."""
    h = hex_color.lstrip("#")
    return tuple(int(h[i:i + 2], 16) for i in (0, 2, 4))

def rgb_to_hex(r: int, g: int, b: int) -> str:
    """Convert RGB values (0–255) to a lowercase hex string."""
    return f"#{r:02x}{g:02x}{b:02x}"

def hex_to_hsl(hex_color: str) -> tuple[float, float, float]:
    """Convert hex to HSL (h: 0–360, s: 0–100, l: 0–100)."""
    r, g, b = hex_to_rgb(hex_color)
    h, l, s = colorsys.rgb_to_hls(r / 255, g / 255, b / 255)
    return h * 360, s * 100, l * 100

def hsl_to_hex(h: float, s: float, l: float) -> str:
    """Convert HSL (h: 0–360, s: 0–100, l: 0–100) to hex."""
    r, g, b = colorsys.hls_to_rgb(h / 360, l / 100, s / 100)
    return rgb_to_hex(int(r * 255), int(g * 255), int(b * 255))

# Round-trip test
brand = "#3b82f6"
r, g, b = hex_to_rgb(brand)
h, s, l = hex_to_hsl(brand)
print(f"  {brand} -> RGB({r}, {g}, {b}) -> HSL({h:.0f}\u00b0, {s:.0f}%, {l:.0f}%)")
print(f"  Back to hex: {hsl_to_hex(h, s, l)}")

تستخدم colorsys أعدادًا عشرية بين 0 و1 لكل قناة، والاسم rgb_to_hls يكشف السر: الناتجان الثاني والثالث هما الإضاءة ثم التشبّع، وليس العكس. الخطأ المبكر في هذا يعني أن كل درجة ستولّدها لاحقًا ستحمل لونًا أو مزاجًا خاطئين ، إنه الخطأ التأسيسي الذي يجب قتله أولًا. وتعكس hsl_to_hex المسار نفسه؛ كلا التحويلين بلا فقدان لقيم RGB الصحيحة.

🎯 الناتج المتوقع : يطبع #3b82f6 -> RGB(59, 130, 246) -> HSL(217°, 91%, 60%) وBack to hex: #3b82f6 (يجانب الذهاب والعودة قيمة الإدخال).

🩹 إذا لم يعمل : إذا لم يطابق hex الناتج عن الجولة الكاملة قيمة الإدخال، فأنت تُمرر التشبّع والإضاءة إلى hsl_to_hex بالترتيب الخاطئ ، بدّل وسيطَي s وl داخل تلك الدالة. وإذا كانت درجة لون معروفة خاطئة بوضوح (تتوقع بنفسجيًّا فتحصل على أحمر)، فالأرجح أنك استدعيت colorsys.rgb_to_hsv بدلًا من rgb_to_hls ، للدالتين شكلان مختلفان للنواتج.

1.2 تحقّق من التحويلات

✅ قائمة التحقق

  • ✅ تمرير لون معروف (#3b82f6) عبر hex -> rgb -> hex وhex -> hsl -> hex يعيد السلسلة الأصلية تمامًا.
  • ✅ يمكنك شرح لماذا HSL هو الفضاء المناسب لعمل اللوحة في الخطوة 2، بينما hex هو الصيغة المناسبة لـ CSS.

🤔 سؤال (أسئلة) سقراطي(ة)

  • لماذا تستخدم دالتا rgb_to_hex وhsl_to_hex كلتاهما int(...) على القيم النهائية ، ماذا قد يحدث لو مرّرت عددًا عشريًا مباشرة؟
  • لو أشرقت لونًا بإضافة 10 إلى كل قناة RGB بدلًا من ضبط الإضاءة في HSL، فهل ستزاح درجة اللون؟ وكيف يمنع HSL هذا النوع من المشاكل؟

الخطوة 2: توليد لوحة الألوان

اللوحة عائلة من الدرجات والصبغات تبدو وكأنها تنتمي معًا. الحيلة بسيطة: ثبّت درجة اللون والتشبّع (تلك هي هوية اللون)، ثم سر على محور الإضاءة فقط. كل سواتش ناتج يتشارك الحمض النووي مع لون العلامة التجارية الأصلي ، يرى المشاهد عائلة، وليس أربعة ألوان متفرقة.

2.1 ابنِ مولّد اللوحة

👟 تلميح البداية : نقّل الإضاءة في HSL بإزاحات ثابتة صعودًا وهبوطًا من القاعدة، مستخدمًا min/max لإبقاء القيم داخل 0–100.

# design_system.py (continued)
def generate_palette(base_hex: str) -> dict:
    """Generate a full palette from a single brand color."""
    h, s, l = hex_to_hsl(base_hex)

    palette = {
        "brand": base_hex,
        "lightest": hsl_to_hex(h, s, min(l + 35, 95)),
        "lighter":  hsl_to_hex(h, s, min(l + 20, 90)),
        "light":    hsl_to_hex(h, s, min(l + 10, 85)),
        "dark":     hsl_to_hex(h, s, max(l - 10, 10)),
        "darker":   hsl_to_hex(h, s, max(l - 20, 5)),
        "darkest":  hsl_to_hex(h, s, max(l - 35, 0)),
    }

    # Grays: desaturated tint of the brand hue, not pure neutral
    for name, lightness in [("gray-100", 96), ("gray-200", 90), ("gray-300", 80),
                            ("gray-400", 60), ("gray-500", 45), ("gray-600", 30),
                            ("gray-700", 20), ("gray-800", 12), ("gray-900", 6)]:
        palette[name] = hsl_to_hex(h, 5, lightness)

    return palette

palette = generate_palette("#3b82f6")
print("Brand palette:")
for name, color in palette.items():
    if not name.startswith("gray"):
        print(f"  {name:>10}: {color}")

الحد الأعلى min(l + 35, 95) يمنع الصبغات الباهتة عند إضاءة 100% ، اللوحة التي تُبقي لمسة من لون العلامة التجارية في أفتح درجة لها متماسكة دائمًا أكثر من الأبيض النقي. وتستخدم الرماديات تشبّعًا ثابتًا بنسبة 5% عند درجة العلامة التجارية بدلًا من 0%، ما يمنحها صبغة دافئة بدلًا من رماد رسمي؛ اختيار تصميمي صغير يجعل اللوحة تبدو ثمينة بهدوء.

🎯 الناتج المتوقع : يطبع brand: #3b82f6 ثم درجات أفتح وأغمق في عائلة اللون نفسها، يتبعها hex العلامة التجارية ثم ستة متغيرات بإزاحات إضاءة.

🩹 إذا لم يعمل : إذا لم يكن المتغير الأفتح هو اللون نفسه بشكل ملحوظ، فالتشبّع منخفض أكثر مما ينبغي أو تجاوزت الإزاحة 95 ، دخلت منطقة شبه الأبيض حيث تكون درجة اللون غير مرئية. وإذا بدا متغيران متجاوران (مثل light وlighter) متطابقين تقريبًا، فإزاحاتك متقاربة جدًا ، باعد بينها.

2.2 تحقّق من اللوحة

✅ قائمة التحقق

  • ✅ كل لون في اللوحة يتشارك زاوية اللون نفسها (217° لـ #3b82f6)، مؤكّدًا باستدعاء hex_to_hsl على كل منها.
  • ✅ أفتح سواتش مميز بوضوح عن الأبيض النقي، وأغمقها ليس أسود بالكامل.

🤔 سؤال (أسئلة) سقراطي(ة)

  • لماذا لا تولّد اللوحة بإضافة 15 إلى قناة الأحمر RGB للون القاعدة بدلًا من السير على إضاءة HSL؟ ماذا يحدث للون المدرك عند تغيير R وG وB بالتساوي؟
  • إذا أعطاك مدير علامة تجارية لونين hex وطلب منك بناء لوحة بكلاهما كنقطتي ارتكاز، فماذا ستحذف أو تصلح في generate_palette ليجعل ذلك ممكنًا؟

الخطوة 3: فحص نسب تباين WCAG

إمكانية الوصول ليست عن الذوق ، إنها عن نسبة. تقول WCAG 2.1 إن النص العادي يحتاج نسبة تباين 4.5:1 ضد خلفيته (AA) و3:1 للنص الكبير؛ وترفع AAA ذلك إلى 7:1. هذه العتبات ملموسة، وأي أداة تولّد لوحات يجب أن تختبرها ، وإلا فأنت تخمّن ما إذا كان نصف مستخدميك يقرؤون الكلمات فعلًا.

3.1 اكتب دوال فحص التباين

👟 تلميح البداية : الإضاءة النسبية في WCAG ليست متوسطًا بسيطًا ، تطبّق خطية sRGB مجزأة تكون أسخى مع قيم القنوات الداكنة. نفّذ الصيغتين تمامًا كما تنص عليهما المواصفة.

# design_system.py (continued)
def relative_luminance(hex_color: str) -> float:
    """Calculate relative luminance per WCAG 2.1 (sRGB linearization)."""
    r, g, b = hex_to_rgb(hex_color)
    channels = []
    for val in (r, g, b):
        srgb = val / 255
        linear = srgb / 12.92 if srgb <= 0.03928 else ((srgb + 0.055) / 1.055) ** 2.4
        channels.append(linear)
    return 0.2126 * channels[0] + 0.7152 * channels[1] + 0.0722 * channels[2]

def contrast_ratio(color1: str, color2: str) -> float:
    """Calculate WCAG contrast ratio between two hex colors."""
    l1, l2 = relative_luminance(color1), relative_luminance(color2)
    lighter, darker = max(l1, l2), min(l1, l2)
    return (lighter + 0.05) / (darker + 0.05)

def check_accessibility(foreground: str, background: str) -> str:
    """Check if a color pair meets WCAG AA and AAA standards."""
    ratio = contrast_ratio(foreground, background)
    aa_normal = ratio >= 4.5
    aa_large  = ratio >= 3.0
    aaa       = ratio >= 7.0
    status = "AAA" if aaa else ("AA" if aa_normal else "Fail")
    size = "normal text" if aa_normal else ("large text" if aa_large else "insufficient")
    return f"  {foreground} on {background}: {ratio:.1f}:1 -> {status} ({size})"

# Test the most common palette combos
print(check_accessibility("#1e293b", "#ffffff"))
print(check_accessibility("#3b82f6", "#ffffff"))
print(check_accessibility("#64748b", "#ffffff"))
print(check_accessibility("#ffffff", "#1e293b"))

صيغة الخطية ، srgb / 12.92 للقيم دون 0.03928، و((srgb + 0.055) / 1.055) ** 2.4 فيما عدا ذلك ، تبدو اعتباطية لكنها تطابق المنحنى الذي ترسمه شاشتك فعلًا. يعكس المتوسط الموزون 0.2126·R + 0.7152·G + 0.0722·B أن الأخضر يحمل معظم الإضاءة في رؤية البشر. إذن لا تخمين حول ما إذا كان #3b82f6 على الأبيض يجتاز AA فعلًا (يجتازه، بالكاد عند ~4.5:1) ، لقد أثبتّ ذلك عدديًا.

🎯 الناتج المتوقع : أربعة أسطر: #1e293b على الأبيض هو AAA (نص عادي)؛ و#3b82f6 على الأبيض هو AA (نص عادي ، بالكاد عند العتبة)؛ و#64748b على الأبيض يفشل في النص العادي لكنه يجتاز النص الكبير؛ و#ffffff على #1e293b يعكس الصف الأول.

🩹 إذا لم يعمل : إذا طبع كل نسبة 1.0:1، فاللونان متطابقان ، مرّرت نفس hex مرتين، أو تُرجع relative_luminance القيمة نفسها لكليهما (أعد التحقق من نقطة قطع فرع الخطية). وإذا فشل زوج يجب أن يجتاز AA، فمن الأرجح أن صيغة الإضاءة تستخدم قنوات RGB بترتيب خاطئ (تحقّق من الأوزان 0.2126/0.7152/0.0722 ، تقابل R وG وB، وليس أي ترتيب آخر).

3.2 تحقّق من فحص التباين

✅ قائمة التحقق

  • ✅ يمكنك تسمية زوج لوحة يجتاز AAA وآخر يفشل في AA ، والتحقق من أن الرقمين يطابقان المواصفة.
  • ✅ يمكنك شرح لماذا تجعل الحالتان «عند 4.5 بالكاد» حول #3b82f6 اختيارات إضاءة مولّد اللوحة ذات عواقب فعلية وليست زخرفية.

🤔 سؤال (أسئلة) سقراطي(ة)

  • تختار مصممة نص gray-400 على خلفية gray-100. النسبة ~5.2:1 ، تجتاز AA. هل ينبغي استخدامها؟ ماذا يحدث لتلك النسبة على شاشة حاسوب محمول رخيص بإعادة إنتاج ضعيفة، وما الذي يقترحه ذلك بشأن بناء هوامش أمان في اللوحة؟
  • أضافت WCAG 2.2 مستوى تباين «محسّنًا». كيف سيتغير الكود، وما القيد الذي ستضيفه إلى check_accessibility لإخراج ثلاث درجات بدلًا من درجتين؟

الخطوة 4: بناء مقياس للطباعة ومقياس مسافات

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

4.1 توليد المقاييس

👟 تلميح البداية : استخدم حجمًا أساسيًا 16 بكسل (CSS rem واحدة افتراضيًا) ونسبة 1.25 («الثالث الأكبر»)، منتجًا 8 تسميات بالضبط ، من xs إلى 3xl.

# design_system.py (continued)
def typography_scale(base: float = 16, ratio: float = 1.25, steps: int = 8) -> dict:
    """Generate a typographic scale from a base size and a modular ratio."""
    labels = ["xs", "sm", "base", "md", "lg", "xl", "2xl", "3xl"]
    scale = {}
    for i, label in enumerate(labels[:steps]):
        size = base * (ratio ** (i - 2))
        scale[label] = {
            "size_px": round(size, 1),
            "size_rem": round(size / 16, 3),
            "line_height": round(1.2 + 0.1 * (steps - i) / steps, 2),
        }
    return scale

def spacing_scale(base: float = 4, steps: int = 10) -> dict:
    """Generate a linear spacing scale in pixels."""
    return {f"{i + 1}": base * (i + 1) for i in range(steps)}

typo = typography_scale()
spacing = spacing_scale()
for label, props in typo.items():
    print(f"  {label:>4}: {props['size_px']:>5.1f}px = {props['size_rem']}rem  (line-height {props['line_height']})")
print("  spacing:", spacing)

الإزاحة (i - 2) تعني أن base (الفهرس 2) يقابل 16 بكسل تمامًا، مع كون xs وsm أصغر وlg3xl أكبر ، القاعدة في المنتصف تمامًا، حيث يعيش معظم نص المحتوى. spacing_scale خطي عمدًا (1×، 2×، …، 10× القاعدة) وليس هندسيًا، لأن الهوامش والحشوات تنمو جمعيًا في التخطيط لا ضربيًا ، الفرق بين «مقياس ينمو كما يفكر المصممون» و«مقياس ينمو كما تشعر الرياضيات».

🎯 الناتج المتوقع : يطبع xs: 10.2px = 0.64rem (الأصغر)، وbase: 16.0px = 1.0rem، حتى 3xl: 39.1px = 2.44rem؛ بينما تطبع المسافات {1: 4, 2: 8, …, 10: 40}.

🩹 إذا لم يعمل : إذا خرج xs وsm معكوسين، فإزاحتك (i + 2) بدلًا من (i - 2). وإذا كانت كل ارتفاعات الأسطر متطابقة، فقد بسّط التعبير إلى ثابت ، تأكّد من اختلاف حدّ (steps - i).

4.2 تحقّق من المقياس

✅ قائمة التحقق

  • base في مقياس الطباعة يساوي 16 بكسل و1.0 rem تمامًا.
  • ✅ كل درجة في مقياس الطباعة تساوي 1.25× درجة ما قبلها تمامًا (ضمن التقريب).

🤔 سؤال (أسئلة) سقراطي(ة)

  • ماذا يحدث إذا غيّرت النسبة من 1.25 إلى 1.333 («الرابع التام»)؟ أي العناوين تنمو أكثر، ومتى تختار إحداهما دون الأخرى؟
  • لماذا لا تستخدم spacing_scale لأحجام الخطوط أيضًا؟ ما الذي يجعل المقياس الخطي (12، 16، 20، 24…) ينهار مع العناوين؟

الخطوة 5: إنشاء بطاقة سواتش اللوحة

ملف لوحة من سلاسل hex مفيد لحاسوب، لا لشخص. بطاقة السواتش هي اللوحة نفسها كصورة ، مرئية وقابلة للقراءة فورًا وقابلة للمشاركة. تمنحك أيضًا فرصة للتأكيد، بالنظر، أن اللوحة تبدو فعلًا كما وعدت به الأرقام.

5.1 ابنِ أداة عرض السواتش

👟 تلميح البداية : استخدم matplotlib.patches.Rectangle لرسم كتلة مملوءة واحدة لكل لون في اللوحة، ثم أضف تسمية نصية. أبقِ المحور مطفأً والتخطيط مشدودًا.

# design_system.py (continued)
import matplotlib.pyplot as plt
import matplotlib.patches as mpatches

def render_palette(palette: dict, filepath: str = "palette.png") -> None:
    """Render a vertical swatch card showing every color in the palette."""
    colors = list(palette.items())
    fig, ax = plt.subplots(figsize=(8, 0.55 * len(colors)))
    for i, (name, hex_color) in enumerate(colors):
        ax.add_patch(mpatches.Rectangle((0, i), 1, 1, color=hex_color, edgecolor="white", linewidth=2))
        ax.text(0.52, i + 0.3, f"{name}: {hex_color}", fontsize=8, color="#1e293b", fontfamily="monospace")
    ax.set_xlim(0, 1)
    ax.set_ylim(0, len(colors))
    ax.axis("off")
    plt.tight_layout()
    plt.savefig(filepath, dpi=150)
    print(f"Saved {filepath}")
    plt.show()

render_palette(palette)

كل Rectangle يشغل العرض الكامل (من 0 إلى 1) ووحدة ارتفاع واحدة (من i إلى i+1)، مع حواف بيضاء تخلق فاصلًا بصريًا بين السواتش. لون النص #1e293b (فحمي داكن، أسود تقريبًا) مكتوب يدويًا ليبقى مقروءًا فوق السواتش الفاتحة ، في أداة إنتاجية ستحوّل شرطيًا إلى نص أبيض فوق الألوان الداكنة. تعرض plt.show() في دفتر ويب أو تفتح نافذة محليًا؛ وتكتب plt.savefig ملف PNG مهما كان ذلك.

🎯 الناتج المتوقع : ملف PNG palette.png يحوي 15 سواتشًا عموديًا (7 درجات العلامة + 9 درجات رمادية)، كل منها مُسمّى بخط monospace، مع إما نافذة matplotlib أو عرض ضمني.

🩹 إذا لم يعمل : إذا خرجت التسميات من الحافة اليمنى، فالشكل أضيق من اللازم ، ارفع عرض figsize. وإذا أصبحت التسميات غير مقروءة على السواتش الداكنة، فيجب أن يتحول لون النص color="#1e293b" إلى الأبيض؛ تبسيط معروف وخفيف ضمن نطاق المولّد الحالي. وإذا أظهر الشكل 3 سواتش فقط، فقد اقتُطع قاموس اللوحة في الطباعة ، تأكّد من وجود المفاتيح الـ15 كاملة قبل الاستدعاء.

5.2 تحقّق من بطاقة السواتش

✅ قائمة التحقق

  • palette.png يحتوي 15 صفًا مُسمّاة تمامًا ويمكن فتحه في أي عارض صور.
  • ✅ يمكنك التأكيد بصريًا أن سواتش gray-* تشترك في الصبغة المتسقة مع لون العلامة التجارية، وليست رمادًا محايدًا نقيًا.

🤔 سؤال (أسئلة) سقراطي(ة)

  • إذا قال زميل «السواتش يبدو باهتًا»، فأين في خط الإنتاج ستغيّر التشبّع ، وهل ستغيّره عالميًا (الخطوة 2) أم فقط للعائلة الفرعية الرمادية؟
  • كيف تضيف تسمية hex تختار تلقائيًا نصًا أبيض أو داكنًا بناءً على الإضاءة، وأي دالة من الخطوة 3 تقوم بالفعل بالحساب الذي تحتاجه؟

الخطوة 6: تصدير رموز التصميم كـ CSS

الاختبار الحقيقي لنظام تصميم هو ما إذا كان يمكن لشخص استخدامه دون فهم كيف بُني. متغيرات CSS المخصصة هي الناتج الأكثر قابليةً للحمل: الصق الملف في وسم <link> أو @import، ويستطيع كل مكوّن في المشروع الإشارة إلى --color-brand أو --font-size-lg أو --space-4 من دون معرفة بـ HSL أو المقاييس المعيارية.

6.1 ابنِ دالة التصدير

👟 تلميح البداية : سر على كل قاموس (palette وtypo وspacing)، ونسّق كل قيمة كسطر --variable: value;، واكتب النتيجة المجمّعة في ملف .css.

# design_system.py (continued)
def export_css(palette: dict, typography: dict, spacing: dict, filepath: str = "design-tokens.css") -> str:
    """Export design tokens as a CSS custom properties file."""
    lines = [":root {", "  /* Brand Colors */"]
    for name, color in palette.items():
        lines.append(f"  --color-{name}: {color};")

    lines += ["", "  /* Typography */"]
    for label, props in typography.items():
        lines.append(f"  --font-size-{label}: {props['size_rem']}rem;")
        lines.append(f"  --line-height-{label}: {props['line_height']};")

    lines += ["", "  /* Spacing */"]
    for step, value in spacing.items():
        lines.append(f"  --space-{step}: {value}px;")

    lines += ["}"]
    css = "\n".join(lines)
    with open(filepath, "w") as f:
        f.write(css)
    print(f"Design tokens exported to {filepath}")
    return css

css_output = export_css(palette, typo, spacing)
print("\n" + css_output)

كل قسم يعلّق على فئته (/* Brand Colors */ و/* Typography */ و/* Spacing */) لأن الملف سيُلصق في نهاية المطاف في قاعدة أكواد يقرؤها شخص غيرك. اختيار rem لا px لأحجام الخطوط مقصود ، يرث إعدادات تكبير المتصفح وهو المعيار لـ CSS سهل الوصول ومتجاوب. طباعة محتوى الملف إلى stdout في النهاية يمنحك تأكيدًا بصريًا فوريًا أن البنية صحيحة، حتى قبل فتح ملف CSS في محرّر.

🎯 الناتج المتوقع : يُكتب design-tokens.css، ويُطبع محتواه إلى stdout: :root { مع 15 خاصية مخصصة --color-*، و16 زوج --font-size-*/--line-height-*، و10 قيم --space-* ، 42 رمزًا إجمالًا.

🩹 إذا لم يعمل : إذا كان ملف CSS فارغًا أو ينقصه :root، تحقّق من أن lines تُجمَّع وتُكتب ، return مبكر قبل open() هو السبب المعتاد. وإذا بدا سطر مثل --color-brand: #3b82f6 من دون فاصلة منقوطة، فسلسلة f تنقصها ; ، الرمز مكسور نحويًا وسيبتلع صامتًا كل خاصية تليه في كتلة القاعدة نفسها.

6.2 تحقّق من التصدير

✅ قائمة التحقق

  • design-tokens.css موجود، ويبدأ بـ :root {، ويحتوي 42 خاصية مخصصة في الأقسام الثلاثة الصحيحة.
  • ✅ لصق سطر واحد ، h1 { color: var(--color-brand); font-size: var(--font-size-xl); } ، في أي ملف HTML يتحول إلى القيم الصحيحة.

🤔 سؤال (أسئلة) سقراطي(ة)

  • يستخدم الملف المُصدَّر px للمسافات وrem لأحجام الخطوط. لماذا خلط الوحدات صحيح هنا، وماذا سيحدث لو استخدمت px لـ font-size أيضًا ، تحديدًا، ماذا يحدث عند زيادة إعداد تكبير المتصفح؟
  • إذا أردت توفير نظام التصميم نفسه في وضعَي الفاتح والداكن، فأين في هذا الخط ستُدخل تصدير لوحة ثانية، وكيف ستركّب CSS لتتبدل تلقائيًا؟

⚠️ المآزق الشائعة

  • الخلط بين colorsys.rgb_to_hls وrgb_to_hsv. للدالتين شكلان مختلفان للنواتج، وتتبادلان موضع التشبّع. إذا كانت الألوان المولدة خاطئة تمامًا، اطبع الناتج الخام لـ hex_to_hsl قبل أي شيء آخر يمسّه ، الخطأ هناك دائمًا.
  • تثبيت الإضاءة يجعل الدرجات المتجاورة غير قابلة للتمييز. يضع min(l + 35, 95) سقفًا لأفتح درجة، لكن إذا كانت إضاءة اللون الأساسي عالية أصلًا (لنقل علامة باستيل عند 80)، ينهار النصف العلوي بأكمله من اللوحة نحو شبه الأبيض. إصلاح عملي واحد: صغّر أحجام الخطوات أو وسّع المدى ديناميكيًا بناءً على إضاءة القاعدة.
  • أوزان الإضاءة النسبية مطبقة بترتيب قنوات خاطئ. تنص WCAG على 0.2126·R + 0.7152·G + 0.0722·B ، وليس أي تبديل آخر. عكسها ينتج نسبًا خاطئة بمهارة قد تحول نجاح AA إلى فشل AA.
  • الخلط بين rem وpx في مقياس الطباعة. size_rem دائمًا size_px / 16 ، إذا أخرجت قيمة البيكسل بتسمية rem بالخطأ، فسيصبح كل حجم أكبر بـ16× وتنفجر الصفحة كلها.
  • توليد لوحة دون النظر إلى إضاءة اللون الأساسي الموجودة. لون أساسي داكن أُزّاح داكنًا 35 نقطة إضاءة هو أسود فعلًا ، يصبح الطرف الداكن من اللوحة غير مقروء. اختبر المولّد بلون علامة داكن وآخر فاتح قبل تسليمه.

ما بنيته للتو

مولّد نظام تصميم مكتمل بذاته: اخترت لون علامة تجارية واحدًا، وأنتج السكربت عائلة كاملة من الدرجات والرماديات، وتحقق من كل زوج نص/خلفية مقابل قواعد إمكانية الوصول WCAG، وولّد مقياس طباعة ومسافات بنسبة متسقة رياضيًا، ورسم بطاقة سواتش قابلة للمشاركة بصيغة PNG، وصدّر 42 خاصية CSS مخصصة جاهزة للإسقاط في أي مشروع ويب. لا يتطلب أي شيء في الناتج عين مصمم لاستخدامه ، أي مطوّر واجهات أمامية يستطيع استيراد ملف CSS والإشارة إلى --color-brand دون فتح مصدر بايثون أبدًا.

شغّل نسخة أكمل دون أي إعداد محلي

examples/design-system/ في مستودع المقرر نسخة دفتر قابلة للتشغيل من كل خطوة أعلاه: الصق لون العلامة التجارية، وشغّل كل الخلايا، واحصل على صورة اللوحة وملف CSS في تنفيذ دفتر واحد. انسخه، أو افتح المستودع كاملًا في GitHub Codespace، وشغّله من هناك.

إلى أين تذهب من هنا

  • أضف متغير وضع داكن: اعكس قيم الإضاءة (بدّل l بـ 100 - l) مع تثبيت درجة اللون والتشبّع، ثم صدّر ملف CSS ثانيًا تحت استعلام وسائط @media (prefers-color-scheme: dark) ليتحول النظام تلقائيًا.
  • ابنِ صفحة HTML لمعاينة اللوحة: ولّد دليل نمط حيًا يعرض كل لون وكل حجم خط وكل قيمة مسافة في استخدام حقيقي ، عناوين بالمقياس، وعروض حشو عند كل مستوى مسافة ، واخدمه محليًا أثناء ضبطك لنظام التصميم.
  • أضف رموز مكوّنات (حشوة الزر، نصف قطر الحدود، ارتفاع الحقل) كقسم /* Components */ جديد في تصدير CSS، مما يجعل نظام التصميم قابلًا للاستهلاك مباشرةً من مكتبة مكوّنات مثل React أو Vue.

شارك مشروعك مع الصف

بنيت شيئًا تفتخر به؟ examples/student-projects/ معرض مشاريع قدّمها طلاب آخرون ، ويحوي README الخاص به شرحًا كاملًا صديقًا للمبتدئين لإضافة مشروعك عبر طلب سحب (pull request)، حتى لو لم تستخدم git من قبل: نسخ المستودع، وإنشاء فرع، والالتزام بملفاتك، وفتح الطلب، خطوة بخطوة. لا يفترض أي خبرة سابقة بـ git.

مرحبًا بك في كتابة بايثون تجعل CSS تفكّر بنفسها. 🎓

أكمل كل خطوة ثم حدد المشروع كمكتمل لجمع نقاطه.