nili/ui

التوثيق

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

التثبيت

الحزمة على نطاق npm خاصّ، لذا يحتاج npm إلى رمز للقراءة فقط قبل أن يراها. ضع الرمز في ‎.npmrc‎ — محليًّا وفي الـCI — وبعدها يصبح التثبيت عاديًا. الإصداران 18 و19 من React مدعومان، ومجموعة الأيقونات peer dependency لتبقى نسختها بيد تطبيقك.

# .npmrc — locally and in CI
@nili:registry=https://registry.npmjs.org/
//registry.npmjs.org/:_authToken=${NILI_TOKEN}

npm i @nili/ui @remixicon/react

المزوّد

يملك NiliProvider قرارين عامّين فقط: أي سمة تُرسم، وبأي اتجاه تُقرأ الصفحة. وهو لا يملك الترجمة عن قصد — المكوّنات تتلقّى نصوصها كخصائص، فتُنشر المكتبة بلا زمن تشغيل i18n وتبقى حرًّا في استخدام i18next أو react-intl أو توجيه Next.js أو لا شيء.

import { NiliProvider } from '@nili/ui';
import '@nili/ui/styles.css';

export default function App({ children }) {
  return (
    <NiliProvider locale="fa" defaultTheme="system">
      {children}
    </NiliProvider>
  );
}

الاتجاه

لا توجد خاصيّة اتجاه، ولا يُقرأ document.dir أثناء الرسم. كُتبت المكوّنات بالخصائص المنطقية، فيُورَّث الاتجاه من CSS — ولهذا تعمل شجرة فرعية من اليمين لليسار داخل صفحة من اليسار لليمين، ويُنتج الرسم على الخادم مخرجات صحيحة من أول مرّة.

// Every component is written this way — logical, not physical.
<div className="ms-4 ps-2 border-s">…</div>

// So a right-to-left island inside a left-to-right page just works,
// with no prop threaded down to make it happen.
<section dir="rtl" lang="fa">
  <Button endIcon={<RiArrowRightLine />}>ادامه</Button>
</section>

الأرقام

تُنسَّق الأرقام على مستوى القيمة عبر Intl، لا بخطّ يضع الأشكال المحلّية فوق نقاط ASCII. هذا الفارق هو ما يجعل البحث داخل الصفحة والنسخ وقارئ الشاشة متّفقين مع ما يُعرض — وما يترك مخرجًا للقيم التي يجب أن تبقى لاتينية، كرقم الآيبان أو رقم الطلب.

import { formatNumber, parseLocalizedNumber } from '@nili/ui';

formatNumber(1234567, { locale: 'fa-IR' });  // ۱٬۲۳۴٬۵۶۷
formatNumber(1234567, { locale: 'ar-EG' });  // ١٬٢٣٤٬٥٦٧
parseLocalizedNumber('۱٬۲۳۴٬۵۶۷');           // 1234567

// And the escape hatch, for a value that must stay Latin:
<TextInput label="IBAN" latin defaultValue="IR82 0540 …" />

الرموز والسمات

لا تكتب المكوّنات متغيّر ‎dark:‎ أبدًا. كل واحد يستدعي رمزًا دلاليًا واحدًا وتتبدّل القيمة تحته، اعتمادًا على السمة data-theme لا على صنف. والسمة مفتوحة، فأي سمة جديدة — تباين عالٍ أو هويّة عميل — هي ملف أنماط لا إعادة كتابة.

--nili-blue-500: #335CFF;        /* primitive  */
--nili-primary-base: …;          /* semantic   */
--color-primary-base: …;         /* @theme     */
class="bg-primary-base"          /* component  */

/* Dark mode is an attribute, not a class — so a third theme
   is [data-theme='high-contrast'] and nothing else changes. */
<html data-theme="dark">

إمكانية الوصول

كل ما لا يحمل تسمية مرئية يطلبها في أنواعه بدل قبول عنصر بلا اسم — زرّ الأيقونة وحدها يتطلّب aria-label، وزرّ الإغلاق يتطلّب dismissLabel. القاعدة أن نظام الأنواع، لا تعليق المراجعة، هو ما يلتقط عنصرًا بلا تسمية.

// The type system refuses an unlabelled control.
<Button iconOnly aria-label="Close" startIcon={<RiCloseLine />} />
<Tag onDismiss={remove} dismissLabel="Remove tag">Design</Tag>
<Alert title="Saved" urgency="polite" />

بعدها تصفّح المكوّنات — يفتح كلٌّ منها على العرض الشامل نفسه الذي يظهر في Storybook، والمصدر إلى جانبه. المكوّنات