پرتوپرتو

ناوبری

الگوهای ناوبری پرتو — Sidebar، NavMenu، Breadcrumb، و اصول طراحی ناوبری

ناوبری خوب به کاربران کمک می‌کند بدانند کجا هستند، کجا می‌توانند بروند، و چطور برگردند. این صفحه الگوها، کامپوننت‌ها، و بهترین روش‌های ناوبری در پرتو را توضیح می‌دهد.


نمونه بصری

مسیریاب — Breadcrumb

تب‌ها — Tabs

محتوای منشن‌ها

معماری ناوبری

پرتو از سه سطح ناوبری استفاده می‌کند:

سطحکامپوننتکاربرد
ناوبری اصلیSidebarدسترسی به بخش‌های اصلی اپلیکیشن
ناوبری محلیNavMenu / Tabsجابجایی بین view های یک بخش
ناوبری مکانیBreadcrumbنمایش موقعیت و مسیر برگشت

ناوبری کناری برای دسترسی به بخش‌های اصلی اپلیکیشن. در پرتو، سایدبار همیشه در سمت راست (start) صفحه قرار می‌گیرد.

;<>
  <>
    <>اصلی</>
    <>
      <>
        <  ={ === '/dashboard'}>
          < ="/dashboard">
            < />
          <>داشبورد</>
          </>
        </>
      </>
      <>
        <  ={.('/influencers')}>
          < ="/influencers">
            < />
          <>اینفلوئنسرها</>
          </>
        </>
      </>
      <>
        <  ={.('/analytics')}>
          < ="/analytics">
            < />
          <>تحلیل‌ها</>
          </>
        </>
      </>
    </>
  </>

  <>
    <>سیستم</>
    <>
      <>
        < >
          < ="/settings">
            < />
          <>تنظیمات</>
          </>
        </>
      </>
    </>
  </>
</>

`SidebarMenuButton` یک دکمه است، نه لینک

این کامپوننت prop‌ای به نام href ندارد و یک <button> رندر می‌کند؛ نسخهٔ پیشین این صفحه href می‌داد، پس تمام سایدبار هیچ‌جا ناوبری نمی‌کرد و مارک‌آپ نامعتبر تولید می‌شد. با asChild بگویید نقش خودش را به فرزندش بدهد و لینک واقعی را داخلش بگذارید. همین برای DropdownMenuItem هم برقرار است — آن هم href ندارد.

آیتم فعال

همیشه آیتم ناوبری فعال را مشخص کنید تا کاربر بداند کجا است:

<SidebarMenuButton asChild isActive={pathname.startsWith('/influencers')}>
          <Link href="/influencers">
            <Users />
  <span>اینفلوئنسرها</span>
          </Link>
        </SidebarMenuButton>

ناوبری محلی زیر سربرگ

برای جابجایی بین view‌های مرتبط در یک بخش — مثل تب‌های یک پروفایل — از Tabs استفاده کنید و آن را داخل PageHeaderNavigationTabs بگذارید تا به لبهٔ پایین سربرگ چسبیده و هم‌تراز شود.

import { , , , ,  } from '@partodata/ui'

;<>
  <>
    < ="overview">
      <>
        < ="overview">خلاصه</>
        < ="posts">پست‌ها</>
        < ="analytics">تحلیل</>
        < ="campaigns">کمپین‌ها</>
      </>
      < ="overview"></>
    </>
  </>
</>

`NavMenu` وجود ندارد

نسخهٔ پیشین این بخش کامپوننتی به نام NavMenu/NavMenuItem را مستند می‌کرد که هیچ‌گاه در دیزاین‌سیستم وجود نداشته — نه در src/index.ts و نه در مانیفست. هر کسی آن کد را کپی می‌کرد با خطای «Element type is invalid» روبرو می‌شد. برای ناوبری محلی Tabs و برای منوی سلسله‌مراتبی NavigationMenu را به کار ببرید.


برای نمایش مسیر فعلی و امکان ناوبری به سطوح بالاتر. معمولاً در PageHeaderBreadcrumb قرار می‌گیرد.

import {
  Breadcrumb,
  BreadcrumbItem,
  BreadcrumbLink,
  BreadcrumbPage,
  BreadcrumbSeparator,
} from '@partodata/ui'

;<Breadcrumb>
  <BreadcrumbItem>
    <BreadcrumbLink href="/campaigns">کمپین‌ها</BreadcrumbLink>
  </BreadcrumbItem>
  <BreadcrumbSeparator />
  <BreadcrumbItem>
    <BreadcrumbLink href="/campaigns/summer-2024">تابستان ۱۴۰۳</BreadcrumbLink>
  </BreadcrumbItem>
  <BreadcrumbSeparator />
  <BreadcrumbItem>
    <BreadcrumbPage>اینفلوئنسرها</BreadcrumbPage>
  </BreadcrumbItem>
</Breadcrumb>

قوانین Breadcrumb

  • صفحه فعلی (BreadcrumbPage) لینک ندارد — کاربر هم‌اکنون آنجاست
  • سطح اول (خانه) را فقط در صورت نیاز نمایش دهید — اگر صفحه در سطح عمیق‌تر از ۲ باشد
  • بیش از ۳-۴ سطح نمایش ندهید — اگر عمق بیشتر بود، سطوح میانی را با ... کوتاه کنید

برای ناوبری ثانویه یا گزینه‌های کاربر در هدر:

import {
  DropdownMenu,
  DropdownMenuTrigger,
  DropdownMenuContent,
  DropdownMenuItem,
  DropdownMenuSeparator,
} from '@partodata/ui'
import { User, Settings, LogOut } from 'lucide-react'

;<DropdownMenu>
  <DropdownMenuTrigger asChild>
    <Button variant="ghost" size="sm">
      <Avatar src={user.avatar} />
      <span>{user.name}</span>
    </Button>
  </DropdownMenuTrigger>
  <DropdownMenuContent align="end">
    <DropdownMenuItem asChild>
      <Link href="/profile"><User className="me-2 h-4 w-4" />
      پروفایل</Link>
    </DropdownMenuItem>
    <DropdownMenuItem asChild>
      <Link href="/settings"><Settings className="me-2 h-4 w-4" />
      تنظیمات</Link>
    </DropdownMenuItem>
    <DropdownMenuSeparator />
    <DropdownMenuItem onClick={handleLogout} className="text-destructive">
      <LogOut className="me-2 h-4 w-4" />
      خروج از حساب
    </DropdownMenuItem>
  </DropdownMenuContent>
</DropdownMenu>

Pagination

برای ناوبری بین صفحات نتایج جستجو یا لیست‌های طولانی:

import {
  Pagination,
  PaginationContent,
  PaginationItem,
  PaginationLink,
  PaginationNext,
  PaginationPrevious,
} from '@partodata/ui'

;<Pagination>
  <PaginationContent>
    <PaginationItem>
      <PaginationPrevious href={`?page=${currentPage - 1}`} />
    </PaginationItem>
    {pages.map((page) => (
      <PaginationItem key={page}>
        <PaginationLink href={`?page=${page}`} isActive={page === currentPage}>
          {page}
        </PaginationLink>
      </PaginationItem>
    ))}
    <PaginationItem>
      <PaginationNext href={`?page=${currentPage + 1}`} />
    </PaginationItem>
  </PaginationContent>
</Pagination>

بهترین روش‌ها

نمایش حالت فعال

همیشه آیتم فعال را مشخص کنید — کاربر باید بداند کجاست:

// با pathname از Next.js
const pathname = usePathname()

<SidebarMenuButton isActive={pathname === '/dashboard'}>
  داشبورد
</SidebarMenuButton>

آیکون‌ها در ناوبری

آیکون‌ها خوانایی ناوبری را بهبود می‌دهند — اما باید همیشه همراه متن باشند:

// درست — آیکون + متن
<SidebarMenuButton>
  <LayoutDashboard aria-hidden="true" />
  <span>داشبورد</span>
</SidebarMenuButton>

// نادرست — آیکون تنها (بدون متن، بدون aria-label)
<SidebarMenuButton>
  <LayoutDashboard />
</SidebarMenuButton>

ناوبری با صفحه‌کلید

  • Tab / Shift+Tab: جابجایی بین آیتم‌های ناوبری
  • Enter / Space: فعال‌سازی لینک
  • ↑ ↓: ناوبری در dropdown menu
  • Escape: بستن dropdown

عمق ناوبری

از ساختار سه‌لایه استفاده کنید — بیشتر از آن گیج‌کننده است:

Sidebar (اصلی)
  └── NavMenu / Tabs (محلی)
         └── Breadcrumb (مکانی)

دام‌های رایج

اشتباهات پرتکراری که در پیاده‌سازی ناوبری دیده می‌شوند — هر مورد شامل اشتباه، دلیل، و الگوی درست است.

خواص فیزیکی CSS در آیتم‌های ناوبری

اشتباه: جابه‌جا کردن شمارنده یا نشان به انتهای آیتم سایدبار با ml-auto، یا تورفتگی زیرمنو با pl-4 — معمولاً با کپی‌کردن نمونه‌های LTR.

// نادرست — margin فیزیکی؛ در RTL شمارنده به‌جای لبهٔ انتهایی کنار متن می‌چسبد
<SidebarMenuButton asChild>
          <Link href="/campaigns">
            <Megaphone />
  <span>کمپین‌ها</span>
  <Badge className="ml-auto">۱۲</Badge>
          </Link>
        </SidebarMenuButton>

// درست — Logical Properties در هر دو جهت درست کار می‌کند
<SidebarMenuButton asChild>
          <Link href="/campaigns">
            <Megaphone />
  <span>کمپین‌ها</span>
  <Badge className="ms-auto">۱۲</Badge>
          </Link>
        </SidebarMenuButton>

چرا دردسرساز است: سایدبار پرتو در لبهٔ آغازین (start) صفحه قرار می‌گیرد و چیدمان flex آن از dir سند پیروی می‌کند، اما margin و padding فیزیکی از جهت صفحه پیروی نمی‌کنند — نتیجه، آیتمی است که در پیش‌نمایش LTR درست به نظر می‌رسد و در محصول RTL به سمت اشتباه می‌چسبد. سیستم طراحی RTL-first است و در کد خودش قانون no-physical-css-properties را با ESLint اجبار می‌کند؛ در کد مصرف‌کننده نیز همیشه ms/me/ps/pe را به‌کار ببرید. برای شمارندهٔ آیتم سایدبار، زیرکامپوننت آمادهٔ SidebarMenuBadge نیز وجود دارد که خودش با مقدار منطقی end-1 در لبهٔ انتهایی قرار می‌گیرد.

استایل‌دهی دستی آیتم فعال با رنگ hardcode

اشتباه: مشخص‌کردن آیتم فعال با کلاس رنگ مستقیم به‌جای پراپ isActive.

// نادرست — رنگ hardcode؛ در تم تیرهٔ پیش‌فرض کنتراست ندارد و aria-current هم تنظیم نمی‌شود
<SidebarMenuButton className={pathname === '/dashboard' ? 'bg-gray-100 text-blue-600' : ''}>
  <LayoutDashboard />
  <span>داشبورد</span>
</SidebarMenuButton>

// درست — isActive هم استایل توکن‌محور می‌دهد و هم aria-current="page"
<SidebarMenuButton isActive={pathname === '/dashboard'}>
  <LayoutDashboard />
  <span>داشبورد</span>
</SidebarMenuButton>

چرا دردسرساز است: سیستم طراحی dark-first است — تم پایهٔ :root تیره است و bg-gray-100 روی آن به لکه‌ای روشن و ناسازگار تبدیل می‌شود که با تغییر تم هم اصلاح نمی‌شود. پراپ isActive استایل فعال را از طریق ویژگی data-active و توکن‌های --sidebar-accent اعمال می‌کند که در هر دو تم مقدار درست دارند، و علاوه بر آن aria-current="page" را نیز روی آیتم تنظیم می‌کند تا صفحه‌خوان‌ها موقعیت فعلی کاربر را اعلام کنند — با رنگ دستی هر دو را از دست می‌دهید.

آیکون جهت‌دار دستی به‌جای جداکنندهٔ پیش‌فرض

اشتباه: جایگزین‌کردن جداکنندهٔ پیش‌فرض Breadcrumb با آیکون جهت‌دار ثابت، با این استدلال که «در RTL فلش باید رو به چپ باشد».

// نادرست — جهت آیکون ثابت شده و فقط در یکی از دو جهت درست است
<BreadcrumbSeparator>
  <ChevronLeft />
</BreadcrumbSeparator>

// درست — جداکنندهٔ پیش‌فرض خودش جهت‌آگاه است
<BreadcrumbSeparator />

چرا دردسرساز است: جداکنندهٔ پیش‌فرض ChevronRight را با کلاس rtl:rotate-180 رندر می‌کند و در هر دو جهت درست نمایش داده می‌شود؛ هر children که بدهید جایگزین کامل آن می‌شود و این چرخش را از دست می‌دهد. همین اصل دربارهٔ Pagination نیز برقرار است: PaginationPrevious و PaginationNext آیکون خود را بر اساس جهت سند در جاوااسکریپت جابه‌جا می‌کنند (چون شماره‌های صفحه عمداً dir="ltr" دارند و واریانت‌های rtl: روی آن‌ها اثر ندارند) — آیکون جهت‌دار دستی به آن‌ها اضافه نکنید. اگر جداکنندهٔ متفاوتی لازم دارید، از نویسهٔ بدون جهت مانند «/» استفاده کنید.

محاسبهٔ تعداد صفحات از طول آرایهٔ صفحهٔ فعلی

اشتباه: ساختن شماره‌های Pagination از rows.length وقتی rows فقط ردیف‌های صفحهٔ فعلی است که سرور برگردانده.

// نادرست — rows فقط ۲۰ ردیف صفحهٔ فعلی است؛ همیشه «صفحهٔ ۱ از ۱» محاسبه می‌شود
const totalPages = Math.ceil(rows.length / pageSize)

// درست — تعداد کل را سرور گزارش می‌کند
const totalPages = Math.ceil(data.totalCount / pageSize)

چرا دردسرساز است: در دادهٔ صفحه‌بندی‌شدهٔ سمت سرور، کلاینت فقط یک صفحه را در اختیار دارد و تعداد کل از روی آن قابل استنتاج نیست؛ نتیجه، صفحه‌بندی‌ای است که همیشه یک صفحه نشان می‌دهد و بقیهٔ داده‌ها را عملاً از دسترس خارج می‌کند. کامپوننت آمادهٔ DataTable سیستم طراحی به همین دلیل سرور-محور طراحی شده است: totalPages و totalRows را به‌صراحت از شما می‌گیرد و خودش از دادهٔ یک صفحه حدس نمی‌زند. برای صفحات لیست کامل، الگوی صفحهٔ جدول داده را ببینید.

لحن محاوره‌ای در برچسب‌های ناوبری

اشتباه: نوشتن برچسب منوها و لینک‌های بازگشت با فارسی محاوره‌ای.

// نادرست — لحن محاوره‌ای و جملهٔ دستوری
<BreadcrumbLink href="/campaigns">برگرد به کمپین‌ها</BreadcrumbLink>

// درست — فارسی رسمی؛ برچسب Breadcrumb نام مقصد است، نه جمله
<BreadcrumbLink href="/campaigns">کمپین‌ها</BreadcrumbLink>

چرا دردسرساز است: برچسب‌های ناوبری پرتکرارترین متن محصول‌اند و کاربر سازمانی در هر جلسه بارها آن‌ها را می‌بیند؛ لحن محاوره‌ای در همین نقطه‌ها بیش از هر جای دیگر به چشم می‌آید و اعتماد را کم می‌کند. زبان سیستم طراحی در همهٔ محصولات پرتو فارسی رسمی است («بازگشت» نه «برگرد»، «مشاهده کنید» نه «ببین»). راهنمای کامل لحن در صفحهٔ محتوا و لحن آمده است.


صفحات مرتبط

  • اشتباهات رایج — فهرست کامل ضدالگوهای سراسری (رنگ hardcode، خواص فیزیکی CSS و …)؛ دام‌های این صفحه نمونه‌های همان ریشه‌ها در این الگو هستند.
  • پوستهٔ برنامه (App Shell) — اگر می‌خواهید سایدبار، هدر، و بوم محتوا را به‌صورت آماده و سیم‌کشی‌شده داشته باشید، به‌جای سرهم‌کردن دستی این اجزا از آن الگو شروع کنید.
  • Sidebar — مرجع کامل props و زیرکامپوننت‌های ناوبری اصلی؛ وقتی به حالت جمع‌شونده، نسخهٔ موبایل، یا کنترل برنامه‌ای وضعیت باز/بسته نیاز دارید.
  • Tabs — اگر view ها آدرس مستقل در URL ندارند و فقط محتوای یک صفحه را تقسیم می‌کنند، به‌جای NavMenu از Tabs استفاده کنید.
  • صفحهٔ جدول داده — وقتی مقصد ناوبری یک صفحهٔ لیست است، صفحه‌بندی و مرتب‌سازی سرور-محور را از این الگو بردارید.