اشتباهات رایج
الگوهایی که باید از آنها پرهیز کنید — بر اساس مشکلات واقعی در محصولات پرتو
اشتباهات رایج
این صفحه اشتباهات پرتکرار را که در محصولات پرتو دیده شدهاند مستند میکند. هر آیتم یک نام، توضیح، مشکل، و راهحل دارد.
رنگهای Hardcode
مشکل: استفاده از رنگهای مستقیم Tailwind به جای توکنهای سیستم.
// ❌ غلط
<span className="text-gray-500">توضیحات</span>
<div className="bg-gray-900 text-white">محتوا</div>
<div style={{ color: '#22c55e' }}>موفقیت</div>
// ✅ درست
<span className="text-muted-foreground">توضیحات</span>
<div className="bg-foreground text-background">محتوا</div>
<Badge variant="success">موفقیت</Badge>چرا اشتباه است: رنگهای hardcode در تم dark کار نمیکنند و WCAG contrast را نقض میکنند.
پیچیدن توکن در hsl()
مشکل: نوشتن hsl(var(--brand-default)) به عادتِ نسخههای قدیمی.
// ❌ غلط — رنگ نامعتبر میسازد، بیصدا
<div className="bg-[hsl(var(--brand-default))]">محتوا</div>
// ✅ درست — توکن از قبل یک رنگ کامل است
<div className="bg-[var(--brand-default)]">محتوا</div>
<div className="bg-brand">محتوا</div>چرا اشتباه است: از نسخهٔ دوم به بعد توکنها رنگ کاملاند، نه سه کانال جدا. پس
hsl(...) دور آنها یک مقدار بیمعنا میسازد و مرورگر بیصدا از خیرش میگذرد: عنصر بدون رنگ
میماند و هیچ خطایی در کنسول نیست. خطرناکیاش همین است که ظاهر کد توکنمحور است، پس در
بازبینی رد میشود. برای شفافیت از /alpha تِیلویند (bg-brand/10) یا color-mix استفاده کنید.
آزمودن فقط در تم روشن
مشکل: رابط را فقط در تم روشن دیدن و همان را ارسال کردن.
چرا اشتباه است: پرتو تیره-اول است — تم پایهٔ :root تیره است و روشن یک opt-in صریح زیر
[data-theme='light']. اپی که هیچ نشانگری روی <html> نگذارد تیره رندر میشود، پس تنها تمی که
آزمودهاید همان تمی است که پیشفرض نیست.
بدتر از آن، تم دو نشانگر دارد که باید همگام بمانند: کلاس (.dark/.light) و attribute
(data-theme). اگر فقط کلاس ست شود، توکنهای سیستم طراحی عوض نمیشوند و خرابی بیصداست —
نه خطایی، نه هشداری، فقط رنگهای اشتباه.
سیمکشی درست در نصب و راهاندازی و تمبندی.
CSS Properties فیزیکی در RTL
مشکل: استفاده از ml، mr، pl، pr، left، right به جای Logical Properties.
// ❌ غلط — در RTL چیدمان معکوس میشود
<div className="ml-4 pl-6 border-l-2 text-left">محتوا</div>
// ✅ درست — با RTL و LTR هر دو کار میکند
<div className="ms-4 ps-6 border-s-2 text-start">محتوا</div>چرا اشتباه است: در محیط RTL (فارسی)، ml به چپ اضافه میشود اما انتظار داریم به راست اضافه شود.
| فیزیکی ❌ | Logical ✅ |
|---|---|
ml-* | ms-* |
mr-* | me-* |
pl-* | ps-* |
pr-* | pe-* |
text-left | text-start |
text-right | text-end |
چند دکمه Primary در یک صفحه
مشکل: بیش از یک variant="primary" در یک view.
// ❌ غلط — کاربر نمیداند کجا کلیک کند
<div className="flex gap-2">
<Button variant="primary">ذخیره</Button>
<Button variant="primary">انتشار</Button>
<Button variant="primary">پیشنویس</Button>
</div>
// ✅ درست — سلسلهمراتب واضح
<div className="flex gap-2">
<Button variant="primary">انتشار</Button>
<Button variant="default">ذخیره</Button>
<Button variant="outline">پیشنویس</Button>
</div>چرا اشتباه است: primary باید یک عمل اصلی در هر view داشته باشد. وقتی همه چیز مهم است، هیچ چیز مهم نیست.
Dialog به جای Drawer در موبایل
مشکل: استفاده از Dialog در صفحههای موبایل که فضای کافی ندارند.
// ❌ غلط — Dialog در موبایل فضای کمی دارد
<Dialog>
<DialogContent>
<FilterForm />
</DialogContent>
</Dialog>
// ✅ درست — Drawer از پایین صفحه باز میشود
<Drawer>
<DrawerContent>
<FilterForm />
</DrawerContent>
</Drawer>راهحل بهتر: از responsive pattern استفاده کنید — Dialog در desktop، Drawer در موبایل.
ساختن کامپوننت Domain از صفر
مشکل: ساختن EngagementRate، SentimentBadge، یا PlatformMark از صفر.
// ❌ غلط — چرخ را دوباره اختراع نکنید
function MyEngagementRate({ rate }: { rate: number }) {
const color = rate > 5 ? 'green' : rate > 3 ? 'yellow' : 'red'
return <div style={{ color }}>{rate}%</div>
}
// ✅ درست — استانداردهای دامنه داخل کامپوننت کد شده
import { EngagementRate } from '@partodata/ui'
;<EngagementRate currentRate={0.042} followers={15000} />کامپوننتهای domain-specific موجود:
EngagementRate— 5 دسته اینفلوئنسر × 6 سطح تعاملEngagementRate— نوار سادهترSentimentBadge/Distribution kind="sentiment"— سه نوع احساسPlatformMark— 7 پلتفرم با رنگ برندPost/Comment/AccountوEntityCollection(@partodata/ui/social) — پست، نظر، حساب و فهرستشان
Compound Component ناقص
مشکل: استفاده از MetricCard، Card، یا Table بدون sub-component های لازم.
// ❌ غلط — MetricCard بدون MetricCardContent
<MetricCard>
<MetricCardHeader>
<MetricCardLabel>کاربران</MetricCardLabel>
</MetricCardHeader>
<p>1,234</p>
</MetricCard>
// ✅ درست
<MetricCard>
<MetricCardHeader>
<MetricCardLabel>کاربران</MetricCardLabel>
</MetricCardHeader>
<MetricCardContent>
<MetricCardValue>1,234</MetricCardValue>
<MetricCardDifferential direction="up" tone="positive">+5٪</MetricCardDifferential>
</MetricCardContent>
</MetricCard>Input بدون FormField در react-hook-form
مشکل: استفاده مستقیم از Input در form های react-hook-form بدون FormField.
// ❌ غلط — validation و error message کار نمیکند
<form onSubmit={handleSubmit(onSubmit)}>
<Input {...register('email')} />
</form>
// ✅ درست — FormPage (فرم، کارت و دکمهٔ ثبت) و هر فیلد یک FormRow
<Form {...form}>
<FormPage title="نمایهٔ من" onSubmit={form.handleSubmit(onSubmit)} onCancel={() => form.reset()} submitLabel="ذخیره">
<FormField
control={form.control}
name="email"
rules={{ required: 'نشانی ایمیل را وارد کنید' }}
render={({ field, fieldState }) => (
<FormRow label="نشانی ایمیل" required error={fieldState.error?.message}>
<Input kind="email" {...field} />
</FormRow>
)}
/>
</FormPage>
</Form>Skeleton با ابعاد اشتباه
مشکل: Skeleton هایی که شکل محتوای واقعی را منعکس نمیکنند.
// ❌ غلط — Skeleton با ابعاد random
<Skeleton className="h-4 w-24" />
<Skeleton className="h-4 w-48" />
// ✅ درست — Skeleton شکل محتوای واقعی را تقلید میکند
<div className="bg-surface-100 p-4 rounded-lg border space-y-2">
<div className="flex items-center gap-3">
<Skeleton className="h-10 w-10 rounded-full" />
<div className="space-y-2">
<Skeleton className="h-4 w-32" />
<Skeleton className="h-3 w-20" />
</div>
</div>
<Skeleton className="h-4 w-full" />
<Skeleton className="h-4 w-3/4" />
</div>نمودار بدون ظرف اندازهدار
مشکل: گذاشتن نمودار در ظرفی که ارتفاع ندارد.
// ❌ غلط — ظرف ارتفاع ندارد، پس نمودار با اندازهٔ صفر رندر میشود
<div>
<PartoLineChart data={data} />
</div>
// ✅ درست — ارتفاع مشخص روی ظرف
<div className="h-[400px]">
<PartoLineChart data={data} />
</div>چرا اشتباه است: نمودارها اندازهشان را از والد میگیرند (ResponsiveContainer، و
ParentSize در نقشهٔ حرارتی). والدِ بیارتفاع یعنی اندازهٔ صفر، و نتیجه یک ناحیهٔ خالی است بدون
هیچ خطایی. این دقیقاً از آن دسته خرابیهایی است که از بازبینی رد میشود، چون صفحه «تمامشده»
بهنظر میرسد.
نمودار در HTML اولیه وجود ندارد
نمودارها رنگهایشان را در زمان اجرا از DOM میخوانند (useRootStyles)، پس ذاتاً سمتکلاینت هستند و در خروجی سرور رندر
نمیشوند. یعنی خزندهٔ موتور جستجو، مصرفکنندهٔ llms.txt و کاربر بدون JS جای نمودار هیچ نمیبینند. اگر عددی باید
در متن باشد، آن را جای دیگری هم بگذارید — مثلاً یک MetricCard کنار نمودار — و نمودار را برای نمایش الگو نگه
دارید، نه برای رساندن مقدار.
اعلام تغییرات: سه دامی که هر سه در محصولات واقعی همین مجموعه افتادهاند
مودال بهازای هر ریلیز
// ❌ اشتباه — هر یادداشت وقفه ایجاد میکند
{ id: 'saved-filters', channel: 'spotlight', /* … */ }
{ id: 'faster-dashboard', channel: 'spotlight', /* … */ }
// ✅ درست — spotlight یک بودجه است، نه یک سطحِ اهمیت
{ id: 'saved-filters', channel: 'announce', /* … */ }
{ id: 'faster-dashboard', channel: 'silent', /* … */ }چرا اشتباه است: سنجیده شد — هر اپ حدود یک تغییرِ دیدنی در هفته دارد. مودال بهازای هر
ریلیز یعنی چهار وقفه در ماه برای خبری که هیچکدام فوری نیست، و کاربر یاد میگیرد قبل از خواندن
ببندد. defineReleaseFeed سقف «یکی در 30 روز» را گیت میکند و build را میشکند، ولی قاعده مهمتر
از گیت است: بودجهٔ وقفه.
شمارنده روی زنگوله برای خبر خوب
// ❌ اشتباه — عدد یعنی «صفی هست که باید خالی شود»
<WhatsNewBell indicator="count" unseenCount={feed.announcedCount} />
// ✅ درست — نقطه یعنی «چیز خوبی هست»
<WhatsNewBell unseenCount={feed.announcedCount} />چرا اشتباه است: با یک تغییر در هفته آن عدد تقریباً همیشه «1» است، و شمارنده قرارداد
کارِ انجامنشده را وارد سطحی میکند که هیچ کاری از کاربر نمیخواهد. dot پیشفرض است و
پیشفرض درستی است.
شمارهٔ نسخه بهعنوان کلید «دیدهشده»
// ❌ اشتباه — عنوانی که نسخه را اعلام میکند، و «دیدهشده» که با نسخه سنجیده میشود
{ id: 'release-2-4-0', title: 'نسخهٔ 2.4.0 منتشر شد', /* … */ }
if (localStorage.getItem('lastSeenVersion') !== appVersion) openSpotlight()
// ✅ درست — نسخه نامِ نمایشیِ ریلیز است و عنوانی در کار نیست؛ کلید «دیدهشده» شناسهٔ پایدار id است
{
id: 'campaign-compare',
date: '2026-09-21',
channel: 'announce',
version: '2.4.0',
changes: [{ type: 'feature', text: 'دو کمپین را میتوانید کنار هم مقایسه کنید' }],
}
const feed = useWhatsNew({ notes: whatsNewFeed.notes, productKey: 'monitor' })چرا اشتباه است: ریلیز سرتیتر جدا ندارد؛ «نسخهٔ 2.4.0» و تاریخ، خودشان سرتیترِ آناند و روی هر سه سطح نمایش داده
میشوند. عنوانِ «نسخهٔ … منتشر شد» فقط همان را تکرار میکند — ریلیز از 5.0 اصلاً title ندارد.
ولی نسخه فقط نام ریلیز است، نه کلیدش: حالت «دیدهشده» با id و ترتیب آرایه سنجیده میشود. وقتی نسخه کلید شود،
«دیدهشده» با یک منبع دوم مقایسه میشود که رانش میکند. این دقیقاً در یک محصول واقعی این مجموعه رخ داد: فایل نسخه روی
مقدار قدیمی ماند و سه ریلیز هیچ اعلامی نکردند، چون شرطِ باز شدن، برابری با همان فایلِ یخزده بود.
و هرگز یک فیلد «جدید» ذخیره نکنید
محصول دیگری isNew: true را دستی روی جدیدترین یادداشت نوشته بود و هیچچیز برش نمیگرداند — آن نقطهٔ سبز سه ماه
برای همهٔ کاربران روی همهٔ صفحهها روشن بود. «دیدهنشده» باید از ترتیب آرایه مشتق شود، نه از پرچمی که کسی باید
یادش باشد خاموش کند. به همین دلیل ReleaseNote هیچ فیلد isNew ندارد.
صفحات مرتبط
- فارسیمحور بودن — دکترین ارقام و اینکه چرا تبدیل رقم در کد، چهار چیز را میشکند
- رنگها — پالت و کنتراست؛ مرجع همان توکنهایی که بالا نباید hardcode شوند
- الگوهای ریسپانسیو — و بهطور مشخص فلش یکفریمی
useIsMobile()در SSR، که آنجا کامل توضیح داده شده - تمبندی — دو نشانگر تم و اینکه چرا نمودارها رنگ را در زمان اجرا میخوانند
- الگوهای بارگذاری — اسکلتون با ابعاد درست، ادامهٔ دام بالا
- راهنمای انتخاب کامپوننت — وقتی مطمئن نیستید کدام کامپوننت
- اعلام تغییرات محصول — بودجهٔ وقفه و اینکه کدام ریلیز سزاوار کدام سطح است