يستخدم جديرون نظام إدخال موحَّدًا مبنيًّا على نظام تصميم جديرون. يتشارك كل حقل نصّي ومنطقة نص ومُشغِّل قائمة اختيار في مساحة عمل المسؤول وبوابة الموظف مصدرًا واحدًا للحقيقة: 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.5 | padding-inline-start | حشو الجهة البادئة (يسار في LTR، يمين في RTL) |
pe-3.5 | padding-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)
| الخاصية | النوع | الافتراضي | الوصف |
|---|---|---|---|
name | string | — | اسم حقل React Hook Form (تدوين النقطة للمسارات المتداخلة) |
label | ReactNode | — | نص التسمية. احذفه للتسميات المخفية بصريًا (أضِف aria-label إلى الإدخال بدلًا من ذلك) |
type | text | email | password | number | tel | url | textarea | search | "text" | نوع الإدخال. استخدم "textarea" لعرض <Textarea> |
placeholder | string | — | نص العنصر النائب |
hint | ReactNode | — | نص مساعدة يظهر أسفل الحقل عند عدم وجود خطأ |
required | boolean | false | يضيف نجمة حمراء إلى التسمية |
optional | boolean | false | يضيف شارة "(اختياري)" إلى التسمية |
rows | number | 4 | عدد صفوف منطقة النص |
disabled | boolean | false | يعطّل الحقل ويطبّق أنماط التعطيل |
className | string | — | يُدمَج على <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". الصفحات في المستوى الأعلى (رؤوس الأقسام) لا تحتاج مسار تتبّع.