الگوهای فرم
راهنمای طراحی فرمهای قابل استفاده، دسترسپذیر، و سازگار با RTL در پرتو
اصول طراحی فرم
فرمها رایجترین نقطه تعامل کاربر با محصول هستند. یک فرم خوب:
- واضح است — کاربر میداند چه باید وارد کند
- بازخورد میدهد — خطاها و موفقیتها فوراً نمایش داده میشوند
- مقاوم در برابر خطا است — وقتی اشتباهی رخ میدهد، کاربر میداند چه کاری بکند
- قابل دسترسی است — با صفحهکلید و screen reader کار میکند
نمونه بصری
ساختار پایه
// Form* ship from their own subpath entry, not the main barrel (RSC compatibility).
import { , , , , , } from '@partodata/ui/form'
import { , } from '@partodata/ui'
import { } from 'react-hook-form'
interface ExampleFormValues {
: string
}
export function () {
const = <ExampleFormValues>({ : { : '' } })
function (: ExampleFormValues) {
.()
}
return (
< {...}>
< ={.()} ="space-y-4">
<
={.}
="username"
={({ }) => (
<>
<>نام کاربری</>
<>
< ="نام کاربری خود را وارد کنید" {...} />
</>
< />
</>
)}
/>
< ="submit">ثبت</>
</>
</>
)
}نمایش خطا
خطای inline (پایین فیلد)
<FormField
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>ایمیل</FormLabel>
<FormControl>
<Input type="email" {...field} />
</FormControl>
<FormMessage /> {/* خطا اینجا نمایش داده میشود */}
</FormItem>
)}
/>خطای کلی فرم (بالای فرم)
{form.formState.errors.root && (
<Alert variant="destructive">
<AlertTitle>خطا در ارسال</AlertTitle>
<AlertDescription>
{form.formState.errors.root.message}
</AlertDescription>
</Alert>
)}فرمهای چند مرحلهای
پیشرفت مراحل را با Stepper نمایش دهید، نه با نوار دستساز — وضعیت هر مرحله (تکمیلشده/فعال/در انتظار)، شمارهگذاری فارسی، و aria-label را خودش مدیریت میکند. مقدار activeStep از صفر شروع میشود.
import { } from 'react'
import { } from 'react-hook-form'
// Form* ship from their own subpath entry, not the main barrel (RSC compatibility).
import { , , , , , } from '@partodata/ui/form'
import { , , , } from '@partodata/ui'
interface SignupFormValues {
: string
: string
}
export function () {
// Stepper is 0-based: 0 = «اطلاعات پایه».
const [, ] = (0)
const = <SignupFormValues>({
: 'onTouched',
: { : '', : '' },
})
function (: SignupFormValues) {
.()
}
return (
< {...}>
< ={.()} ="space-y-6">
< ={}>
< ="اطلاعات پایه" />
< ="اطلاعات تماس" />
< ="بازبینی" />
</>
{/* مرحله ۱ */}
{ === 0 && (
< ="space-y-4">
< ="text-lg font-semibold">اطلاعات پایه</>
<
={.}
="name"
={{ : 'نام الزامی است' }}
={({ }) => (
<>
<>نام</>
<>
< ="نام خود را وارد کنید" {...} />
</>
< />
</>
)}
/>
{/* htmlType defaults to "button", so a step change never submits the form */}
< ={() => (1)}>مرحله بعد</>
</>
)}
{/* مرحله ۲ */}
{ === 1 && (
< ="space-y-4">
< ="text-lg font-semibold">اطلاعات تماس</>
<
={.}
="email"
={{ : 'ایمیل الزامی است' }}
={({ }) => (
<>
<>ایمیل</>
<>
< ="email" ="ltr" ="example@domain.com" {...} />
</>
< />
</>
)}
/>
< ="flex gap-2">
< ="outline" ={() => (0)}>قبلی</>
< ={() => (2)}>مرحله بعد</>
</>
</>
)}
{/* مرحله ۳ — ارسال */}
{ === 2 && (
< ="flex gap-2">
< ="outline" ={() => (1)}>قبلی</>
< ="submit" ="primary" ={..}>
ثبت
</>
</>
)}
</>
</>
)
}Validation
با Zod
import { z } from 'zod'
import { zodResolver } from '@hookform/resolvers/zod'
const schema = z.object({
username: z
.string()
.min(3, 'نام کاربری باید حداقل ۳ کاراکتر باشد')
.max(20, 'نام کاربری نباید بیش از ۲۰ کاراکتر باشد'),
email: z
.string()
.email('آدرس ایمیل نامعتبر است'),
password: z
.string()
.min(8, 'رمز عبور باید حداقل ۸ کاراکتر باشد'),
})
const form = useForm({
resolver: zodResolver(schema),
defaultValues: { username: '', email: '', password: '' },
})دسترسیپذیری فرمها
// همیشه label با input مرتبط باشد
<FormLabel htmlFor="email">ایمیل</FormLabel>
<Input id="email" aria-describedby="email-error" />
<FormMessage id="email-error" />
// فیلدهای اجباری مشخص شوند
<FormLabel>
ایمیل
<span aria-hidden="true" className="text-destructive ms-1">*</span>
<span className="sr-only"> (اجباری)</span>
</FormLabel>
// خطاها با role="alert" اعلام شوند
<div role="alert" aria-live="polite">
<FormMessage />
</div>بهترین روشها و دامهای رایج
بهترین روشها
- label را بالای فیلد قرار دهید، نه کنار آن — برای RTL و LTR هر دو بهتر است
- خطا را پایین فیلد مربوطه نمایش دهید، نه در یک خلاصه کلی
- دکمه submit را پس از آخرین فیلد قرار دهید
- از placeholder برای مثال استفاده کنید، نه برای توضیح
- فیلدهای اجباری را با
*مشخص کنید - بعد از submit موفق، یک feedback واضح نمایش دهید (toast یا success state)
دامهای رایج
۱. ویژگیهای فیزیکی CSS در چیدمان RTL
اشتباه: استفاده از کلاسهای جهتدار فیزیکی مانند pl-10، ml-2 یا text-left برای جای دادن آیکون داخل input، فاصله ستاره فیلد اجباری، یا تراز پیام خطا.
چرا مشکلساز است: صفحههای پرتو RTL هستند؛ کلاس فیزیکی عنصر را به سمت اشتباه میبرد — padding آیکون روی متن ورودی میافتد و ستاره اجباری به جای بعد از label، قبل از آن ظاهر میشود. داخل خود پکیج قانون ESLint no-physical-css-properties این الگو را رد میکند، اما در کد مصرفکننده هیچ محافظی وجود ندارد.
الگوی درست: همیشه از ویژگیهای منطقی استفاده کنید (ml → ms، mr → me، pl → ps، pr → pe، text-left → text-start):
// ❌ نادرست — در RTL آیکون روی متن میافتد
<Input className="pl-10 text-left" />
// ✅ درست — در RTL و LTR هر دو صحیح است
<Input className="ps-10 text-start" />۲. رنگ هاردکد برای حالت خطا
اشتباه: نمایش پیام خطا با text-red-500 یا style={{ color: '#ef4444' }} به جای توکن معنایی.
چرا مشکلساز است: توکنهای پرتو رنگ کامل هستند و در هر تم مقدار مناسب همان تم را دارند؛ رنگ ثابت فقط برای یک تم تنظیم شده و در تم دیگر کنتراست کافی ندارد. علاوه بر این، FormMessage خودش با توکن text-destructive رندر میشود — قرمز دستی شما با آن یکسان نخواهد بود و فرم دو سایه قرمز متفاوت پیدا میکند.
الگوی درست: خطا را به <FormMessage /> بسپارید و هر متن خطای دستی را با توکن معنایی رنگ کنید:
// ❌ نادرست — فقط در یک تم درست دیده میشود
<p className="text-red-500">آدرس ایمیل نامعتبر است</p>
// ✅ درست — توکن در هر دو تم مقدار صحیح دارد
<p className="text-destructive">آدرس ایمیل نامعتبر است</p>۳. نادیده گرفتن تم تیره (dark-first)
اشتباه: ساخت کارت فرم با bg-white dark:bg-zinc-900 و بررسی ظاهر فقط در تم روشن.
چرا مشکلساز است: تم پایه پرتو تیره است — مصرفکنندهای که هیچ تمی تنظیم نکند، تیره رندر میشود؛ bg-white بدون پوشش کامل حالت تیره یعنی یک کارت سفید خیرهکننده وسط داشبورد تیره. واریانتهای دستی dark: هم نسخه دومی از تصمیم رنگی میسازند که با بهروزرسانی توکنها همگام نمیماند.
الگوی درست: یک کلاس توکن واحد که در هر دو تم مقدار درست دارد، و بررسی فرم ابتدا در تم تیره:
// ❌ نادرست — دو تصمیم رنگی جدا که از توکنها عقب میمانند
<div className="bg-white dark:bg-zinc-900 border-gray-200 rounded-md p-6">
// ✅ درست — توکن در هر دو تم خودش حل میشود
<div className="bg-surface-100 border border-default rounded-md p-6">۴. فیلتر سمت کلاینت وقتی فرم به DataTable وصل است
اشتباه: فرم جستجو یا فیلتر (مثلاً فیلتر نتایج «کمپین تخفیف فصلی») کل مجموعه داده را یکجا fetch میکند و فیلتر و صفحهبندی در مرورگر انجام میشود.
چرا مشکلساز است: DataTable پرتو از پایه server-paged طراحی شده است — مقدار totalRows را باید سرور گزارش کند و جدول نمیتواند آن را از دادههای صفحه فعلی استنتاج کند. با حجم واقعی دادههای پایش (هزاران ردیف)، fetch کامل زمان بارگذاری و حافظه را میبلعد و state فرم با state جدول از هم جدا میافتد.
الگوی درست: مقادیر فرم را بهصورت پارامتر query به سرور بفرستید و پاسخ صفحهبندیشده را مستقیم به جدول بدهید:
// سرور بر اساس مقادیر فرم فیلتر میکند و فقط یک صفحه برمیگرداند
const { data, totalRows, totalPages } = usePagedSearch({
query: form.watch('query'),
page,
pageSize,
})
<DataTable
columns={columns}
data={data}
pagination={{
currentPage: page,
totalPages,
onPageChange: setPage,
pageSize,
onPageSizeChange: setPageSize,
totalRows, // خلاصه «۱–۲۰ از ۱٬۲۴۰» را فعال میکند
}}
/>۵. لحن غیررسمی یا ناهمگون در label و پیام خطا
اشتباه: «ایمیلت رو وارد کن»، «رمزت خیلی کوتاهه» — یا ترکیب لحن رسمی و غیررسمی در فیلدهای مختلف یک فرم.
چرا مشکلساز است: زبان استاندارد پرتو فارسی رسمی است و مخاطب آن تحلیلگران و تیمهای سازمانی هستند؛ لحن غیررسمی یا ناهمگون اعتبار محصول را کم میکند و پیام خطایی که در یک فیلد رسمی و در فیلد بعدی محاورهای است، فرم را ترجمهنشده و ناتمام جلوه میدهد.
الگوی درست: «آدرس ایمیل خود را وارد کنید»، «رمز عبور باید حداقل ۸ کاراکتر باشد» — همان لحنی که در پیامهای schema بخش Validation همین صفحه به کار رفته است. قواعد کامل نوشتار در محتوا و لحن آمده است.
صفحات مرتبط
- اشتباهات رایج — فهرست کامل ضدالگوهای سراسری (رنگ hardcode، خواص فیزیکی CSS و …)؛ دامهای این صفحه نمونههای همان ریشهها در این الگو هستند.
- الگوهای خطا — نمایش خطاهای API و سطح صفحه
- محتوا و لحن — قوانین نوشتن label، placeholder، و پیام خطا
- دسترسیپذیری — ارتباط label با input، role="alert"