پرتوپرتو

اشتباهات رایج

الگوهایی که باید از آن‌ها پرهیز کنید — بر اساس مشکلات واقعی در محصولات پرتو

اشتباهات رایج

این صفحه اشتباهات پرتکرار را که در محصولات پرتو دیده شده‌اند مستند می‌کند. هر آیتم یک نام، توضیح، مشکل، و راه‌حل دارد.


رنگ‌های 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>
<span className="text-brand">موفقیت</span>

چرا اشتباه است: رنگ‌های 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-lefttext-start
text-righttext-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، یا SocialPlatformBadge از صفر.

// ❌ غلط — چرخ را دوباره اختراع نکنید
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 — ۵ دسته اینفلوئنسر × ۶ سطح تعامل
  • EngagementRateBar — نوار ساده‌تر
  • SentimentBadge / SentimentDistribution — سه نوع احساس
  • SocialPlatformBadge — ۷ پلتفرم با رنگ برند
  • ProfileCard / ProfileInfo — پروفایل اینفلوئنسر

Compound Component ناقص

مشکل: استفاده از MetricCard، Card، یا Table بدون sub-component های لازم.

// ❌ غلط — MetricCard بدون MetricCardContent
<MetricCard>
  <MetricCardHeader>
    <MetricCardLabel>کاربران</MetricCardLabel>
  </MetricCardHeader>
  <p>۱,۲۳۴</p>
</MetricCard>

// ✅ درست
<MetricCard>
  <MetricCardHeader>
    <MetricCardLabel>کاربران</MetricCardLabel>
  </MetricCardHeader>
  <MetricCardContent>
    <MetricCardValue>۱,۲۳۴</MetricCardValue>
    <MetricCardDifferential variant="positive">+۵٪</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>

// ✅ درست
<Form {...form}>
  <form onSubmit={form.handleSubmit(onSubmit)}>
    <FormField
      control={form.control}
      name="email"
      render={({ field }) => (
        <FormItem>
          <FormLabel>ایمیل</FormLabel>
          <FormControl><Input {...field} /></FormControl>
          <FormMessage />
        </FormItem>
      )}
    />
  </form>
</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 سقف «یکی در ۳۰ روز» را گیت می‌کند و build را می‌شکند، ولی قاعده مهم‌تر از گیت است: بودجهٔ وقفه.

شمارنده روی زنگوله برای خبر خوب

// ❌ اشتباه — عدد یعنی «صفی هست که باید خالی شود»
<WhatsNewBell indicator="count" unseenCount={feed.announcedCount} />

// ✅ درست — نقطه یعنی «چیز خوبی هست»
<WhatsNewBell unseenCount={feed.announcedCount} />

چرا اشتباه است: با یک تغییر در هفته آن عدد تقریباً همیشه «۱» است، و شمارنده قرارداد کارِ انجام‌نشده را وارد سطحی می‌کند که هیچ کاری از کاربر نمی‌خواهد. dot پیش‌فرض است و پیش‌فرض درستی است.

شمارهٔ نسخه به‌عنوان هویت یادداشت

// ❌ اشتباه — کاربر نسخه را دنبال نمی‌کند
title: 'نسخهٔ ۲.۴.۰ منتشر شد'

// ✅ درست — عنوان می‌گوید کاربر حالا چه می‌تواند بکند
title: 'حالا می‌توانید دو کمپین را کنار هم مقایسه کنید'

چرا اشتباه است: هویتِ یک ورودی، تاریخ آن است نه نسخه‌اش. اعتبارسنج، semver در عنوان را رد می‌کند. و خطر جدی‌تر: وقتی نسخه هویت شود، وسوسه می‌شوید حالت «دیده‌شده» را هم با نسخه مقایسه کنید — که یعنی یک منبع دوم که رانش می‌کند. این دقیقاً در یک محصول واقعی این مجموعه رخ داد: فایل نسخه روی مقدار قدیمی ماند و سه ریلیز هیچ اعلامی نکردند، چون شرطِ باز شدن، برابری با همان فایلِ یخ‌زده بود.

و هرگز یک فیلد «جدید» ذخیره نکنید

محصول دیگری isNew: true را دستی روی جدیدترین یادداشت نوشته بود و هیچ‌چیز برش نمی‌گرداند — آن نقطهٔ سبز سه ماه برای همهٔ کاربران روی همهٔ صفحه‌ها روشن بود. «دیده‌نشده» باید از ترتیب آرایه مشتق شود، نه از پرچمی که کسی باید یادش باشد خاموش کند. به همین دلیل ReleaseNote هیچ فیلد isNew ندارد.


صفحات مرتبط