{"id":"l-ds-3-1","courseId":"design-systems","lang":"ar","kind":"lesson","summary":"صمّم component API يستخدم variants من نوع enum بدل الأعلام المنطقية، ومفردات props موحّدة، وتمريرًا للسمات الأصلية، وslots للمحتوى، مع Button الخاص بـ Northwind مثالًا.","html":"<p>كان لـ Button الذي أطلقته Northwind عام 2021 تسعة props: <code dir=\"ltr\">primary</code>، <code dir=\"ltr\">secondary</code>، <code dir=\"ltr\">danger</code>، <code dir=\"ltr\">small</code>، <code dir=\"ltr\">large</code>، <code dir=\"ltr\">outline</code>، <code dir=\"ltr\">block</code>، <code dir=\"ltr\">icon</code>، و<code dir=\"ltr\">onPress</code>. والقيم المنطقية (boolean) السبع منها وحدها تسمح بـ 128 تركيبة، ووجدت الفرق معظمها. كان في التطبيق الإنتاجي <code dir=\"ltr\">&lt;Button primary danger small large&gt;</code>. كان يعرض شيئًا ما، ولم يستطع أحد أن يقول ماذا كان يُفترض أن يكون. الـ component API عقدٌ مع كل فريق يستخدم المكوّن، وكأي عقد، الجزء الصعب هو ما يسمح به.</p>\n<h2 id=\"sec-1\" tabindex=\"-1\">الـ enums للاختيارات، والقيم المنطقية للحالات</h2>\n<p>القاعدة الأولى تخصّ شكل الـ props. إن كانت الخيارات <strong>متنافية</strong>، فمكانها prop واحد من نوع enum. وإن كان الشيء <strong>يعمل أو لا يعمل</strong>، فهو قيمة منطقية (boolean).</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"tsx\" data-index=\"0\"><div class=\"code-head\"><span class=\"code-title\">Button.tsx</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"نسخ الكود\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>نسخ</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">type</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> ButtonProps</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> React</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">.</span><span style=\"color:#953800;--shiki-dark:#FFA657\">ComponentProps</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'button'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">> </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">&#x26;</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span></span>\n<span class=\"line\"><span style=\"color:#953800;--shiki-dark:#FFA657\">  variant</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">?:</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'primary'</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> |</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'secondary'</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> |</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'danger'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#953800;--shiki-dark:#FFA657\">  size</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">?:</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'sm'</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> |</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'md'</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> |</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'lg'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#953800;--shiki-dark:#FFA657\">  loading</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">?:</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> boolean</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#953800;--shiki-dark:#FFA657\">  startIcon</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">?:</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> React</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">.</span><span style=\"color:#953800;--shiki-dark:#FFA657\">ReactNode</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#953800;--shiki-dark:#FFA657\">  endIcon</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">?:</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> React</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">.</span><span style=\"color:#953800;--shiki-dark:#FFA657\">ReactNode</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">};</span></span></code></pre></div>\n<p>حين يكون <code dir=\"ltr\">variant</code> من نوع enum، لا يمكن أن يكون <code dir=\"ltr\">primary</code> و<code dir=\"ltr\">danger</code> صحيحين معًا؛ ونظام الأنواع يرفض ذلك قبل مراجعة الكود. أما <code dir=\"ltr\">loading</code> فيبقى قيمة منطقية، لأن الزر إما في حالة تحميل أو لا، بشكل مستقل عن الـ variant الخاص به.</p>\n<h2 id=\"sec-2\" tabindex=\"-1\">مفردات واحدة عبر النظام</h2>\n<p>القاعدة الثانية هي الاتّساق بين المكوّنات. إن استخدم Button القيمة <code dir=\"ltr\">size=&quot;sm&quot;</code>، فكذلك يفعل Input وSelect وTabs وBadge، بالقيم الثلاث نفسها. وإن سمّى أحد المكوّنات معالج التغيير <code dir=\"ltr\">onChange</code> ومرّر إليه القيمة الجديدة، فكلها تفعل ذلك. تحتفظ Northwind بمسرد API من صفحة واحدة لأسماء الـ props المشتركة، والمكوّنات الجديدة تعيد استخدام هذه الأسماء ما لم يوجد سبب قوي للخروج عنها.</p>\n<p>وهذا أهمّ من أي قرار منفرد بشأن prop. المهندس الذي استخدم ثلاثة مكوّنات من Northwind يجب أن يستطيع تخمين API المكوّن الرابع بشكل صحيح. وكل تناقض ضريبة صغيرة يدفعها الجميع، كل يوم.</p>\n<h2 id=\"sec-3\" tabindex=\"-1\">لا تحجب المنصّة</h2>\n<p>القاعدة الثالثة: يجب ألا يسلب المكوّن أبدًا ما يقدّمه HTML أصلًا. الـ Button هو <code dir=\"ltr\">&lt;button&gt;</code>، لذا يجب أن يقبل كل ما يقبله <code dir=\"ltr\">&lt;button&gt;</code>، بما في ذلك سمات <code dir=\"ltr\">aria-*</code>، وأن يمرّره:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"tsx\" data-index=\"1\"><div class=\"code-head\"><span class=\"code-title\">Button.tsx</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"نسخ الكود\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>نسخ</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">export</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> function</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> Button</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">({</span></span>\n<span class=\"line\"><span style=\"color:#953800;--shiki-dark:#FFA657\">  variant</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'secondary'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">,</span></span>\n<span class=\"line\"><span style=\"color:#953800;--shiki-dark:#FFA657\">  size</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'md'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">,</span></span>\n<span class=\"line\"><span style=\"color:#953800;--shiki-dark:#FFA657\">  loading</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> false</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">,</span></span>\n<span class=\"line\"><span style=\"color:#953800;--shiki-dark:#FFA657\">  startIcon</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">,</span></span>\n<span class=\"line\"><span style=\"color:#953800;--shiki-dark:#FFA657\">  endIcon</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">,</span></span>\n<span class=\"line\"><span style=\"color:#953800;--shiki-dark:#FFA657\">  type</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'button'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">,</span></span>\n<span class=\"line\"><span style=\"color:#953800;--shiki-dark:#FFA657\">  children</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">,</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  ...</span><span style=\"color:#953800;--shiki-dark:#FFA657\">rest</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> ButtonProps</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) {</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  return</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    &#x3C;</span><span style=\"color:#116329;--shiki-dark:#7EE787\">button</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">      {...</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">rest</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">}</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">      type</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">={</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">type</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">}</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">      className</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">={</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">[</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'nw-button'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, rest.className].</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">filter</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(Boolean).</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">join</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">' '</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">)</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">}</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">      data-variant</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">={</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">variant</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">}</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">      data-size</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">={</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">size</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">}</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">      aria-busy</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">={</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">loading </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">||</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> undefined</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">}</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    ></span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">      {</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">startIcon</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">}</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">      &#x3C;</span><span style=\"color:#116329;--shiki-dark:#7EE787\">span</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> className</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"nw-button__label\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">></span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">{</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">children</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">}</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;/</span><span style=\"color:#116329;--shiki-dark:#7EE787\">span</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">></span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">      {</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">endIcon</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">}</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    &#x3C;/</span><span style=\"color:#116329;--shiki-dark:#7EE787\">button</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">></span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  );</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}</span></span></code></pre></div>\n<p>لأن <code dir=\"ltr\">ref</code> صار prop عاديًا في مكوّنات الدوال في React 19، فإنه يمرّ عبر <code dir=\"ltr\">...rest</code> مع كل شيء آخر، فتعمل إدارة التركيز (focus) والـ tooltips دون تغليف إضافي. وللسبب نفسه تعمل <code dir=\"ltr\">onClick</code> و<code dir=\"ltr\">disabled</code> و<code dir=\"ltr\">form</code> و<code dir=\"ltr\">aria-describedby</code>.</p>\n<aside class=\"callout callout-mistake\" role=\"note\"><p class=\"callout-title\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><path d=\"m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3\"/><path d=\"M12 9v4\"/><path d=\"M12 17h.01\"/></svg><span><span class=\"callout-kind\">خطأ شائع</span><span class=\"callout-sep\" aria-hidden=\"true\"> · </span>وراثة القيمة الافتراضية الأصلية لـ type</span></p>\n<p>عنصر <code dir=\"ltr\">&lt;button&gt;</code> الأصلي داخل نموذج تكون قيمته الافتراضية <code dir=\"ltr\">type=&quot;submit&quot;</code>. خسرت Northwind أسبوعًا بسبب نماذج dispatch تُرسَل حين يضغط الناس «Add stop». صار Button في النظام يستخدم افتراضيًا <code dir=\"ltr\">type=&quot;button&quot;</code>، والفرق تمرّر <code dir=\"ltr\">type=&quot;submit&quot;</code> حين تقصده. القيم الافتراضية قرارات تصميم: اختر القيمة الآمنة حين لا يقرأ الناس التوثيق.</p>\n</aside>\n<h2 id=\"sec-4\" tabindex=\"-1\">الـ slots للمحتوى</h2>\n<p>الـ <strong>slot</strong> (الفتحة) مكان يمرّر فيه المستخدم محتوى بدل الإعدادات. <code dir=\"ltr\">children</code> هو الـ slot الرئيسي؛ و<code dir=\"ltr\">startIcon</code> و<code dir=\"ltr\">endIcon</code> هما slots مسمّاة. قارن بين طريقتين لإضافة أيقونة:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"tsx\" data-index=\"2\"><div class=\"code-head\"><span class=\"code-title\">TSX</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"نسخ الكود\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>نسخ</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;</span><span style=\"color:#116329;--shiki-dark:#7EE787\">Button</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> icon</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"truck\"</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> iconPosition</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"left\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">>Assign&#x3C;/</span><span style=\"color:#116329;--shiki-dark:#7EE787\">Button</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">>        </span><span style=\"color:#6E7781;--shiki-dark:#8B949E\">// configuration</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;</span><span style=\"color:#116329;--shiki-dark:#7EE787\">Button</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> startIcon</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">={</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;</span><span style=\"color:#116329;--shiki-dark:#7EE787\">TruckIcon</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> /></span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">}</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">>Assign&#x3C;/</span><span style=\"color:#116329;--shiki-dark:#7EE787\">Button</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">>               </span><span style=\"color:#6E7781;--shiki-dark:#8B949E\">// slot</span></span></code></pre></div>\n<p>نسخة الإعدادات تحتاج أن يعرف الـ Button كل اسم أيقونة وكل موضع، ويُضاف إليها prop كلما أراد أحد شيئًا جديدًا. أما نسخة الـ slot فتقبل أي عنصر، فتستطيع الفرق تمرير مجموعة أيقونات مختلفة أو صورة رمزية صغيرة دون إصدار جديد. والدرس التالي يأخذ هذه الفكرة أبعد بكثير.</p>\n<h2 id=\"sec-5\" tabindex=\"-1\">الـ variants في CSS: حوّل الـ props إلى سمات</h2>\n<p>يكتب الـ Button قيم الـ props الخاصة به في السمتين <code dir=\"ltr\">data-variant</code> و<code dir=\"ltr\">data-size</code>، فيعكس الـ CSS الـ API واحدًا لواحد. وكل variant يغيّر الـ component tokens وحدها:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"css\" data-index=\"3\"><div class=\"code-head\"><span class=\"code-title\">CSS</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"نسخ الكود\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>نسخ</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">.nw-button</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">[</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">data-variant</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"primary\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">] {</span></span>\n<span class=\"line\"><span style=\"color:#953800;--shiki-dark:#FFA657\">  --nw-button-bg</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">var</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#953800;--shiki-dark:#FFA657\">--nw-color-action-bg</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">);</span></span>\n<span class=\"line\"><span style=\"color:#953800;--shiki-dark:#FFA657\">  --nw-button-text</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">var</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#953800;--shiki-dark:#FFA657\">--nw-color-action-text</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">);</span></span>\n<span class=\"line\"><span style=\"color:#953800;--shiki-dark:#FFA657\">  --nw-button-border</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">transparent</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}</span></span></code></pre></div>\n<p>الـ data attributes تُبقي نقاط ربط الأنماط مقروءة في DevTools، حيث ترى <code dir=\"ltr\">data-variant=&quot;primary&quot;</code> بدل اسم class مولَّد بالـ hash لا يُقرأ، ولا يمكن جمعها في تركيبة عبثية كما يمكن مع classes مثل <code dir=\"ltr\">.primary.danger</code>.</p>\n<aside class=\"callout callout-tip\" role=\"note\"><p class=\"callout-title\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><path d=\"M15 14c.2-1 .7-1.7 1.5-2.5 1-.9 1.5-2.2 1.5-3.5A6 6 0 0 0 6 8c0 1 .2 2.2 1.5 3.5.7.7 1.3 1.5 1.5 2.5\"/><path d=\"M9 18h6\"/><path d=\"M10 22h4\"/></svg><span><span class=\"callout-kind\">نصيحة</span><span class=\"callout-sep\" aria-hidden=\"true\"> · </span>اختر الـ variant الافتراضي عن قصد</span></p>\n<p>الـ variant الافتراضي في Northwind هو <code dir=\"ltr\">secondary</code>. جعل <code dir=\"ltr\">primary</code> اختيارًا صريحًا يدفع الفرق نحو إجراء أساسي واحد في كل عرض، وهو نمط تطلبه مبادئنا أصلًا. القيمة الافتراضية ليست محايدة؛ إنها الـ variant الذي ستراه أكثر من غيره في الإنتاج.</p>\n</aside>\n<h2 id=\"sec-6\" tabindex=\"-1\">راجع الـ API قبل أن تبنيه</h2>\n<p>تكتب Northwind كود الاستخدام قبل كود المكوّن. كل اقتراح مكوّن جديد يبدأ بثلاثة مقاطع: الاستخدام الأكثر شيوعًا، والاستخدام الأعقد الذي طلبه أحد، واستخدام واحد <em>يجب</em> أن يكون مستحيلًا. ثم تطرح المراجعة أربعة أسئلة. هل تطابق أسماء الـ props مسرد الـ API؟ هل يمكن لأي تركيبة من الـ props أن تصف شيئًا بلا معنى؟ هل يمرّر المكوّن كل ما يدعمه عنصره الأصلي؟ وما الذي سيحتاجه الفريقان الثاني والثالث اللذان سيستخدمانه ولم يحتجه الفريق الأول؟</p>\n<p>تستغرق هذه المراجعة نحو ثلاثين دقيقة. أما تغيير الـ API بعد أن يعتمد عليه عشرون فريقًا فيتطلّب إصدارًا رئيسيًا (major)، ودليل انتقال، وشهورًا من المتابعة، وهذا موضوع القسم 4.</p>\n<p>في التمرين تكتب قواعد الـ variant والحجم لهذا الـ Button باستخدام الـ component tokens وحدها. وبعدها ترى ما يحدث حين يتجاوز المكوّن حدود الـ props كليًا.</p>\n","toc":[{"id":"sec-1","text":"الـ enums للاختيارات، والقيم المنطقية للحالات","level":2},{"id":"sec-2","text":"مفردات واحدة عبر النظام","level":2},{"id":"sec-3","text":"لا تحجب المنصّة","level":2},{"id":"sec-4","text":"الـ slots للمحتوى","level":2},{"id":"sec-5","text":"الـ variants في CSS: حوّل الـ props إلى سمات","level":2},{"id":"sec-6","text":"راجع الـ API قبل أن تبنيه","level":2}],"takeaways":["استخدم prop واحدًا من نوع enum مثل <code dir=\"ltr\">variant</code> للخيارات المتنافية؛ فالقيم المنطقية المنفصلة تسمح بتركيبات لا معنى لها.","اجعل أسماء الـ props وقيمها متطابقة عبر المكوّنات، حتى يعني <code dir=\"ltr\">size=&quot;sm&quot;</code> الشيء نفسه على Button وInput وSelect.","مرّر السمات الأصلية والـ <code dir=\"ltr\">ref</code> إلى العنصر الأساسي، حتى لا يحجب المكوّن أبدًا ما يقدّمه HTML أصلًا.","القيم الافتراضية قرارات تصميم؛ اختر الخيار الآمن والشائع، مثل <code dir=\"ltr\">type=&quot;button&quot;</code> والـ variant الثانوي."],"further":[{"title":"<button>: The Button element (MDN)","url":"https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/button"},{"title":"forwardRef, and ref as a prop in React 19 (React docs)","url":"https://react.dev/reference/react/forwardRef"},{"title":"Using data attributes (MDN)","url":"https://developer.mozilla.org/en-US/docs/Web/HTML/How_to/Use_data_attributes"}],"quiz":[{"q":"<p>يقبل Button القديم في Northwind القيم المنطقية <code dir=\"ltr\">primary</code> و<code dir=\"ltr\">danger</code> و<code dir=\"ltr\">outline</code>. كتب أحد الفرق <code dir=\"ltr\">&lt;Button primary danger&gt;</code>. ما مشكلة الـ API الأساسية؟</p>","options":[{"text":"يجب أن تُسمّى القيم المنطقية <code dir=\"ltr\">isPrimary</code> و<code dir=\"ltr\">isDanger</code>.","why":"إعادة التسمية لا تمنع التركيبة المستحيلة؛ المشكلة في شكل الـ API لا في الأسماء."},{"text":"لا يمكن تمرير القيم المنطقية بلا قيمة في JSX.","why":"السمات المنطقية المجرّدة صالحة في JSX وتعني <code dir=\"ltr\">true</code>."},{"text":"الخيارات المتنافية مصمّمة كأعلام مستقلة، فتصبح التركيبات غير الصالحة ممكنة الكتابة.","why":"صحيح. prop واحد من نوع enum باسم <code dir=\"ltr\">variant</code> يجعل الجمع بين <code dir=\"ltr\">primary</code> و<code dir=\"ltr\">danger</code> مستحيلًا."},{"text":"يجب أن يرمي المكوّن خطأً حين يُحدَّد الاثنان.","why":"خطأ وقت التشغيل أفضل من الصمت، لكن يجب أن يجعل الـ API الخطأ مستحيل الكتابة من الأساس."}],"answer":2},{"q":"<p>زرّ Button داخل <code dir=\"ltr\">&lt;form&gt;</code> يواصل إرسال النموذج حين يضغط الناس «Add stop». الـ Button يعرض <code dir=\"ltr\">&lt;button&gt;</code> بلا <code dir=\"ltr\">type</code>. ما أفضل إصلاح داخل نظام التصميم؟</p>","options":[{"text":"توثيق أن على الفرق أن تمرّر دائمًا <code dir=\"ltr\">type=&quot;button&quot;</code>.","why":"التوثيق يساعد، لكن القيمة الافتراضية التي تسبّب الأخطاء ستظلّ تسبّبها لكل من يقرأ على عجل."},{"text":"إزالة عنصر النموذج من المنتج.","why":"النموذج مشروع تمامًا؛ الخطأ في القيمة الافتراضية للمكوّن."},{"text":"إيقاف حدث الإرسال داخل معالج النقر في الـ Button.","why":"هذا يكسر الحالات التي تريد فيها الفرق فعلًا زر إرسال."},{"text":"جعل القيمة الافتراضية لـ <code dir=\"ltr\">type</code> هي <code dir=\"ltr\">&quot;button&quot;</code>، وترك الفرق تمرّر <code dir=\"ltr\">type=&quot;submit&quot;</code> حين تقصده.","why":"صحيح. القيمة الافتراضية الأصلية هي <code dir=\"ltr\">submit</code>، وهي تفاجئ الناس؛ ويستطيع النظام اختيار القيمة الأكثر أمانًا."}],"answer":3},{"q":"<p>أيّ تصميم للـ props هو الأكثر اتّساقًا بين مكوّنات النظام؟</p>","options":[{"text":"Button يستخدم <code dir=\"ltr\">size=&quot;sm&quot;</code>، وInput يستخدم <code dir=\"ltr\">small</code>، وSelect يستخدم <code dir=\"ltr\">compact</code>.","why":"ثلاث صيغ لفكرة واحدة تجبر كل فريق على مراجعة التوثيق في كل مرة."},{"text":"Button وInput وSelect كلها تستخدم <code dir=\"ltr\">size</code> بالقيم <code dir=\"ltr\">sm</code> و<code dir=\"ltr\">md</code> و<code dir=\"ltr\">lg</code>.","why":"صحيح. مفردات واحدة تعني أنك تتعلّمها مرة واحدة وتخمّن بشكل صحيح في المكوّن التالي."},{"text":"كل مكوّن يختار ما يُقرأ أفضل في سياقه.","why":"سهولة القراءة المحلية عبر أربعين فريقًا تتحوّل إلى ارتباك عام."}],"answer":1}],"exercise":{"kind":"web","mode":"dom","starter":{"html":"<div class=\"row\">\n  <button class=\"nw-button\" type=\"button\" id=\"b-default\">Cancel</button>\n  <button class=\"nw-button\" type=\"button\" data-variant=\"primary\" id=\"b-primary\">Assign driver</button>\n  <button class=\"nw-button\" type=\"button\" data-variant=\"danger\" id=\"b-danger\">Delete route</button>\n  <button class=\"nw-button\" type=\"button\" data-variant=\"primary\" data-size=\"sm\" id=\"b-small\">Add stop</button>\n</div>","css":":root {\n  --nw-color-surface: #ffffff;\n  --nw-color-text: #1f2933;\n  --nw-color-border: #c5ccd3;\n  --nw-color-action-bg: #1f5ae0;\n  --nw-color-action-text: #ffffff;\n  --nw-color-danger-bg: #c62828;\n  --nw-color-danger-text: #ffffff;\n}\n\n/* Base = the secondary variant */\n.nw-button {\n  --nw-button-bg: var(--nw-color-surface);\n  --nw-button-text: var(--nw-color-text);\n  --nw-button-border: var(--nw-color-border);\n  --nw-button-padding-y: 8px;\n  --nw-button-padding-x: 16px;\n\n  background: var(--nw-button-bg);\n  color: var(--nw-button-text);\n  border: 1px solid var(--nw-button-border);\n  padding: var(--nw-button-padding-y) var(--nw-button-padding-x);\n  border-radius: 6px;\n  font: inherit;\n}\n.row { display: flex; gap: 8px; flex-wrap: wrap; }\n\n/* Add the primary, danger and sm rules below.\n   Change component tokens only. */\n","js":""},"solution":{"html":"<div class=\"row\">\n  <button class=\"nw-button\" type=\"button\" id=\"b-default\">Cancel</button>\n  <button class=\"nw-button\" type=\"button\" data-variant=\"primary\" id=\"b-primary\">Assign driver</button>\n  <button class=\"nw-button\" type=\"button\" data-variant=\"danger\" id=\"b-danger\">Delete route</button>\n  <button class=\"nw-button\" type=\"button\" data-variant=\"primary\" data-size=\"sm\" id=\"b-small\">Add stop</button>\n</div>","css":":root {\n  --nw-color-surface: #ffffff;\n  --nw-color-text: #1f2933;\n  --nw-color-border: #c5ccd3;\n  --nw-color-action-bg: #1f5ae0;\n  --nw-color-action-text: #ffffff;\n  --nw-color-danger-bg: #c62828;\n  --nw-color-danger-text: #ffffff;\n}\n\n/* Base = the secondary variant */\n.nw-button {\n  --nw-button-bg: var(--nw-color-surface);\n  --nw-button-text: var(--nw-color-text);\n  --nw-button-border: var(--nw-color-border);\n  --nw-button-padding-y: 8px;\n  --nw-button-padding-x: 16px;\n\n  background: var(--nw-button-bg);\n  color: var(--nw-button-text);\n  border: 1px solid var(--nw-button-border);\n  padding: var(--nw-button-padding-y) var(--nw-button-padding-x);\n  border-radius: 6px;\n  font: inherit;\n}\n.row { display: flex; gap: 8px; flex-wrap: wrap; }\n\n.nw-button[data-variant=\"primary\"] {\n  --nw-button-bg: var(--nw-color-action-bg);\n  --nw-button-text: var(--nw-color-action-text);\n  --nw-button-border: transparent;\n}\n.nw-button[data-variant=\"danger\"] {\n  --nw-button-bg: var(--nw-color-danger-bg);\n  --nw-button-text: var(--nw-color-danger-text);\n  --nw-button-border: transparent;\n}\n.nw-button[data-size=\"sm\"] {\n  --nw-button-padding-y: 4px;\n  --nw-button-padding-x: 12px;\n}\n","js":""},"checks":[{"id":"secondary-default","test":"return style('#b-default', 'background-color') === 'rgb(255, 255, 255)' && style('#b-default', 'border-top-color') === 'rgb(197, 204, 211)';"},{"id":"primary","test":"const b = $('#b-primary'); return style(b, 'background-color') === 'rgb(31, 90, 224)' && style(b, 'color') === 'rgb(255, 255, 255)' && style(b, 'border-top-color') === 'rgba(0, 0, 0, 0)';"},{"id":"danger","test":"const b = $('#b-danger'); return style(b, 'background-color') === 'rgb(198, 40, 40)' && style(b, 'color') === 'rgb(255, 255, 255)' && style(b, 'border-top-color') === 'rgba(0, 0, 0, 0)';"},{"id":"small","test":"const b = $('#b-small'); return style(b, 'padding-top') === '4px' && style(b, 'padding-left') === '12px' && style(b, 'background-color') === 'rgb(31, 90, 224)' && style('#b-primary', 'padding-top') === '8px';"},{"id":"tokens-only","test":"const rules = [...document.styleSheets].flatMap((s) => [...s.cssRules]).filter((r) => r.selectorText && /data-(variant|size)/.test(r.selectorText)); return rules.length >= 3 && rules.every((r) => [...r.style].every((p) => p.startsWith('--nw-button-')));"}],"title":"نسّق الـ variants الخاصة بالـ Button بالـ component tokens","prompt":"<p>يعرض الـ Button قيم الـ props الخاصة به كسمتين <code dir=\"ltr\">data-variant</code> و<code dir=\"ltr\">data-size</code>. والقاعدة الأساسية <code dir=\"ltr\">.nw-button</code> هي الـ variant الثانوي. أضف ثلاث قواعد <strong>لا تحدّد إلا component tokens من نوع <code dir=\"ltr\">--nw-button-…</code></strong> (بلا تعريفات <code dir=\"ltr\">background</code> أو <code dir=\"ltr\">color</code> أو <code dir=\"ltr\">padding</code>):</p>\n<ul>\n<li><code dir=\"ltr\">data-variant=&quot;primary&quot;</code>: خلفية الإجراء ونصّه من الـ semantic tokens، وحدّ شفاف.</li>\n<li><code dir=\"ltr\">data-variant=&quot;danger&quot;</code>: خلفية الخطر ونصّه من الـ semantic tokens، وحدّ شفاف.</li>\n<li><code dir=\"ltr\">data-size=&quot;sm&quot;</code>: حشوة 4px عموديًا و12px أفقيًا.</li>\n</ul>\n<p>يجب أن يكون الزر الأساسي الصغير صغيرًا وأساسيًا معًا.</p>\n","hints":["اختر العنصر بالسمة: <code dir=\"ltr\">.nw-button[data-variant=&quot;primary&quot;] { … }</code>.","في الداخل، خصّص الـ tokens التي تستخدمها القاعدة الأساسية أصلًا، مثل <code dir=\"ltr\">--nw-button-bg: var(--nw-color-action-bg);</code>.","حدّد <code dir=\"ltr\">--nw-button-border: transparent;</code> لـ primary وdanger، وtokens الحشوة الاثنين لـ <code dir=\"ltr\">sm</code>."],"explanation":"<p>صار كل variant حفنة من تخصيصات الـ tokens. القاعدة الأساسية تملك <em>كيف</em> تتحوّل الـ tokens إلى خصائص CSS، والـ variants تغيّر فقط <em>أيّ</em> القيم تتدفّق إليها. ولأن الحجم والـ variant يلمسان tokens مختلفة، يتركّب <code dir=\"ltr\">data-variant=&quot;primary&quot; data-size=&quot;sm&quot;</code> دون قاعدة إضافية، والفريق الذي يحتاج تعديلًا منفردًا يستطيع تخصيص component token واحد بدل مصارعة الـ cascade.</p>\n","checkLabels":{"secondary-default":"الزر بلا variant يبقى ثانويًا (أبيض بحدّ رمادي)","primary":"<code dir=\"ltr\">data-variant=&quot;primary&quot;</code> يستخدم ألوان الإجراء مع حدّ شفاف","danger":"<code dir=\"ltr\">data-variant=&quot;danger&quot;</code> يستخدم ألوان الخطر مع حدّ شفاف","small":"<code dir=\"ltr\">data-size=&quot;sm&quot;</code> يقلّص الحشوة ويتركّب مع primary","tokens-only":"قواعد الـ variant والحجم لا تحدّد إلا tokens من نوع <code dir=\"ltr\">--nw-button-…</code>"}},"words":705,"readMinutes":4}