النماذج وحقول الإدخال

للمسؤولين

يستخدم جديرون نظام إدخال موحَّدًا مبنيًّا على نظام تصميم جديرون. يتشارك كل حقل نصّي ومنطقة نص ومُشغِّل قائمة اختيار في مساحة عمل المسؤول وبوابة الموظف مصدرًا واحدًا للحقيقة: components/ui/field-styles.ts. تشرح هذه الصفحة المواصفة البصرية، والأحجام المتاحة، وسلوك الكتابة من اليمين إلى اليسار (RTL)، وحالات التحقق، ومُغلِّف FormField الذي يربط ذلك كله.

وحدة field-styles

تُصدِّر components/ui/field-styles.ts أربعة ثوابت نصّية بسيطة. لا تكلفة وقت تشغيل ولا تبعيات — مجرّد سلاسل أصناف Tailwind يستوردها كل مكوّن إدخال.

التصديريستخدمهالارتفاع الافتراضي
fieldInputClass<Input>، و<input> الخام40 بكسل (h-10)
fieldTextareaClass<Textarea>80 بكسل كحد أدنى (min-h-[80px])
fieldSelectTriggerClass<SelectTrigger>40 بكسل (h-10)
fieldInputSmClass<Input size="sm">، والبحث المضمَّن36 بكسل (h-9)

تتشارك الثوابت الأربعة اللغة البصرية ذاتها: نصف قطر rounded-lg، وحدّ border-input، وتعبئة bg-background، وارتفاع السكون shadow-xs الذي هو توقيع نظام التصميم.

حالة السكون

يُعرَض كل إدخال بالحجم الافتراضي بارتفاع 40 بكسل (h-10). يبقي هذا الحقولَ النصّية وقوائم الاختيار والإدخالات المجاورة للأزرار على خط الأساس ذاته كي تتحاذى التركيبات الأفقية دون تجاوزات ارتفاع يدوية.

أصناف حالة السكون الأساسية (من fieldInputClass):

h-10 w-full min-w-0
rounded-lg border border-input bg-background
shadow-xs
ps-3.5 pe-3.5 py-2
text-sm placeholder:text-muted-foreground
transition-colors

shadow-xs ظلّ إسقاط بعرض بكسل واحد يرفع الحقل قليلًا عن اللوحة — تفصيلة مقصودة في نظام التصميم تُميّز الإدخالات عن مساحات السطح العادية.

حالة التركيز

يستخدم التركيز نهجًا بلا مخطّط (outline) يستبدل حلقة المتصفح الافتراضية بأخرى بعلامة تجارية:

outline-none
focus-visible:border-ring
focus-visible:ring-[3px] focus-visible:ring-ring/40

يجعل تبديل border-ring لونَ الحدّ يقفز إلى رمز العلامة التجارية عند التركيز. وتوفّر الحلقة شبه الشفافة بعرض 3 بكسل هالةً ناعمة تستوفي متطلبات مظهر التركيز في WCAG 2.4.11 دون قسوة المخطّط المصمت.

حالة التحقق / عدم الصلاحية

تتلقّى الحقول غير الصالحة حدًّا وحلقةً بلون تدميري مدفوعَين بسمة aria-invalid. لا حاجة لتبديل أصناف إضافي — يستجيب المتغيّر تلقائيًا:

aria-invalid:border-destructive
aria-invalid:ring-[3px] aria-invalid:ring-destructive/20

ومكافئات الوضع الداكن مضمَّنة أيضًا:

dark:aria-invalid:border-destructive/50
dark:aria-invalid:ring-destructive/40

عيّن aria-invalid={true} على أي إدخال، أو دع FormField يفعل ذلك نيابةً عنك (انظر أدناه).

حالة التعطيل

disabled:pointer-events-none
disabled:cursor-not-allowed
disabled:opacity-50
disabled:bg-input/50
dark:disabled:bg-input/80

تفقد الإدخالات المعطَّلة أحداث المؤشر وتخفت إلى 50% من العتامة. وتنتقل الخلفية إلى bg-input/50 لتعزيز الحالة غير النشطة بصريًا.

الأحجام

يتوفّر حجمان:

الافتراضي — 40 بكسل

استخدم fieldInputClass (أو ببساطة <Input> بلا سمة size). هذا الخيار الصحيح للنماذج وصفحات الإعدادات وأي مكان يقع فيه حقل بجوار التسميات والنصوص الإرشادية.

الصغير — 36 بكسل

استخدم fieldInputSmClass (أو <Input size="sm">). مُخصَّص للسياقات الكثيفة: أشرطة التصفية، والبحث المضمَّن في صفوف الجداول، وعناصر شريط الأدوات. الفرق الوحيد عن الافتراضي هو h-9 وpy-1.5.

تجنّب خلط الأحجام داخل قسم نموذج واحد. وإن احتجت تخطيطًا مُدمجًا، فاستخدم الحجم الصغير باتّساق في ذلك القسم بأكمله.

أمان الكتابة من اليمين إلى اليسار (RTL)

تستخدم كل المسافات الأفقية خصائص CSS المنطقية حصرًا. والخصائص الفيزيائية (pl، pr، left، right) ممنوعة في هذه الوحدة.

صنف Tailwindخاصية CSS المنطقيةالأثر
ps-3.5padding-inline-startحشو الجهة البادئة (يسار في LTR، يمين في RTL)
pe-3.5padding-inline-endحشو الجهة المنتهية (يمين في LTR، يسار في RTL)
ms-*margin-inline-startهامش البداية المضمَّن
me-*margin-inline-endهامش النهاية المضمَّن

عندما تكون سمة dir للصفحة rtl (العربية)، يعكس المتصفح البداية/النهاية تلقائيًا. لا حاجة لأي منطق RTL على مستوى المكوّن.

الوضع الداكن

كل تجاوزات الوضع الداكن مضمَّنة في الثوابت. لا يضيف المستهلكون أصناف dark: يدويًا — فهي تأتي مجانًا مع التصدير.

dark:bg-input/30                    ← تعبئة السكون
dark:disabled:bg-input/80           ← تعبئة التعطيل
dark:aria-invalid:border-destructive/50
dark:aria-invalid:ring-destructive/40

مكوّن FormField

components/portal/form/FormField.tsx هو المُغلِّف المُوصى به لكل إدخال نموذج. يتولّى:

  • ربط Controller الخاص بـ React Hook Form (يقرأ من أقرب <FormProvider>)
  • توليد id وaria-describedby تلقائيًا وبشكل يدعم الوصول
  • عرض التسمية مع نجمة المطلوب الاختيارية وشارة "(اختياري)"
  • نص إرشادي يظهر حين يكون الحقل صالحًا
  • رسالة خطأ تظهر (مع role="alert") حين يكون الحقل غير صالح
  • تمرير aria-invalid إلى <Input> أو <Textarea> الأساسي

الاستخدام الأساسي

import { FormField } from "@/components/portal/form/FormField";

// داخل مكوّن مُغلَّف بـ <FormProvider methods={...}>
<FormField
  name="email"
  label="Work email"
  type="email"
  placeholder="you@example.com"
  required
/>

مع إدخال مخصّص (render prop)

لقوائم الاختيار، وصناديق البحث المنسدلة، ومنتقيات التاريخ، أو أي إدخال غير قياسي، مرِّر عنصرًا فرعيًا من نوع render-prop:

<FormField name="sector" label="Sector" required>
  {({ id, value, onChange, onBlur, invalid, describedBy }) => (
    <Select value={value} onValueChange={onChange}>
      <SelectTrigger
        id={id}
        aria-invalid={invalid}
        aria-describedby={describedBy}
      >
        <SelectValue placeholder="Select sector" />
      </SelectTrigger>
      <SelectContent>
        {sectors.map((s) => (
          <SelectItem key={s.id} value={s.id}>{s.name}</SelectItem>
        ))}
      </SelectContent>
    </Select>
  )}
</FormField>

مرجع الخصائص (Props)

الخاصيةالنوعالافتراضيالوصف
namestring—اسم حقل React Hook Form (تدوين النقطة للمسارات المتداخلة)
labelReactNode—نص التسمية. احذفه للتسميات المخفية بصريًا (أضِف aria-label إلى الإدخال بدلًا من ذلك)
typetext | email | password | number | tel | url | textarea | search"text"نوع الإدخال. استخدم "textarea" لعرض <Textarea>
placeholderstring—نص العنصر النائب
hintReactNode—نص مساعدة يظهر أسفل الحقل عند عدم وجود خطأ
requiredbooleanfalseيضيف نجمة حمراء إلى التسمية
optionalbooleanfalseيضيف شارة "(اختياري)" إلى التسمية
rowsnumber4عدد صفوف منطقة النص
disabledbooleanfalseيعطّل الحقل ويطبّق أنماط التعطيل
classNamestring—يُدمَج على <div> المُغلِّف الخارجي
children(args: RenderArgs) => ReactNode—render-prop للإدخالات المخصّصة

رفيق SaveBar

تُصدِّر FormField.tsx أيضًا SaveBar — شريطًا سفليًا لاصقًا لنماذج نمط الإعدادات:

import { SaveBar } from "@/components/portal/form/FormField";

<SaveBar
  dirty={form.formState.isDirty}
  saving={isSaving}
  saveLabel="Save changes"
  cancelLabel="Cancel"
  onCancel={() => form.reset()}
/>

لا يعرض SaveBar شيئًا حين تكون dirty غير صحيحة، فلا حاجة لتركيبه شرطيًا.

التوسيع أو التجاوز

تقبل كل الإدخالات خاصية className. استخدم cn() من @/lib/utils لدمج أصناف إضافية دون إفساد الأنماط الأساسية:

import { cn } from "@/lib/utils";
import { fieldInputClass } from "@/components/ui/field-styles";

// إدخال خام يختار الانضمام إلى المظهر الموحَّد
<input className={cn(fieldInputClass, "font-mono")} />

لا تتجاوز الارتفاع أو الحشو أو نصف قطر الحدّ أو الظلّ أو أصناف التركيز دون تحديث field-styles.ts — فالتغيير من نقطة واحدة يبقي كل الإدخالات متّسقة.

ما هو غير متوفّر في v0.5

  • متغيّر حجم كبير / XL. كل الإدخالات بحجم 40 بكسل أو 36 بكسل. متغيّر كبير بحجم 44 بكسل لأزرار الحثّ البارزة ضمن قائمة انتظار v0.6.
  • خانات أيقونة بادئة/لاحقة. نمط InputGroup (أيقونة أو نص بادئ/لاحق مثبَّت داخل الحدّ) في بدائية منفصلة components/ui/input-group.tsx. وهي تُغلِّف fieldInputClass وتضبط الحشو عبر متغيّرات CSS.
  • Combobox. صندوق البحث المنسدل بدائية منفصلة مبنيّة على Radix UI. يستخدم مُشغِّله fieldInputClass كأساس.

التنقّل الداخلي: مسار التتبّع (Breadcrumbs)

كل صفحة داخلية في مساحة عمل المسؤول أو بوابة الموظف — أي نموذج أو صفحة تفاصيل أو صفحة تعديل أو صفحة متفرّعة — تُظهِر موقعها عبر مكوّن مسار تتبّع موحَّد بدل أزرار العودة الخاصة بكل صفحة (مثل «العودة إلى الوظائف» أو «العودة إلى الأوصاف الوظيفية»). هذه قاعدة تصميم دائمة: لا أزرار عودة مخصّصة بعد الآن.

  • المكوّن الوحيد المعتمد هو components/app/shell/Breadcrumbs.tsx.
  • يُعرَض في خانة eyebrow من PageHeader (فوق عنوان الصفحة مباشرة) كي يكون موضعه ثابتًا في كل الصفحات.
  • يفصل بين العناصر سهمٌ يتبع اتجاه اللغة تلقائيًّا (ChevronLeft في العربية، ChevronRight في الإنجليزية)، فلا حاجة لقلب يدوي.
  • يجب أن يكون المسار دقيقًا للموقع وينتهي بالصفحة الحالية. مثال لصفحة تعديل وظيفة: المستوى الأعلى ← الوظائف ← «اسم الوظيفة» ← تعديل.
import { Breadcrumbs } from "@/components/app/shell/Breadcrumbs";
import { PageHeader } from "@/components/portal/PageHeader";

<PageHeader
  eyebrow={
    <Breadcrumbs
      locale={locale}
      items={[
        { label: c.crumbJobs, href: `/${orgSlug}/app/positions` },
        { label: title, href: `/${orgSlug}/app/positions/${id}` },
        { label: c.crumbEdit }, // العنصر الأخير بلا href = الصفحة الحالية
      ]}
    />
  }
  title={c.title}
  description={c.description}
/>

العنصر الأخير في items يُترَك بلا href فيُعرَض كنصّ ثابت يحمل aria-current="page". الصفحات في المستوى الأعلى (رؤوس الأقسام) لا تحتاج مسار تتبّع.