ناوبری
الگوهای ناوبری پرتو — Sidebar، NavMenu، Breadcrumb، و اصول طراحی ناوبری
ناوبری خوب به کاربران کمک میکند بدانند کجا هستند، کجا میتوانند بروند، و چطور برگردند. این صفحه الگوها، کامپوننتها، و بهترین روشهای ناوبری در پرتو را توضیح میدهد.
نمونه بصری
مسیریاب — Breadcrumb
تبها — Tabs
معماری ناوبری
پرتو از سه سطح ناوبری استفاده میکند:
| سطح | کامپوننت | کاربرد |
|---|---|---|
| ناوبری اصلی | Sidebar | دسترسی به بخشهای اصلی اپلیکیشن |
| ناوبری محلی | NavMenu / Tabs | جابجایی بین view های یک بخش |
| ناوبری مکانی | Breadcrumb | نمایش موقعیت و مسیر برگشت |
Sidebar
ناوبری کناری برای دسترسی به بخشهای اصلی اپلیکیشن. در پرتو، سایدبار همیشه در سمت راست (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 را به کار ببرید.
Breadcrumb
برای نمایش مسیر فعلی و امکان ناوبری به سطوح بالاتر. معمولاً در 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) لینک ندارد — کاربر هماکنون آنجاست - سطح اول (خانه) را فقط در صورت نیاز نمایش دهید — اگر صفحه در سطح عمیقتر از ۲ باشد
- بیش از ۳-۴ سطح نمایش ندهید — اگر عمق بیشتر بود، سطوح میانی را با
...کوتاه کنید
Dropdown Navigation
برای ناوبری ثانویه یا گزینههای کاربر در هدر:
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 menuEscape: بستن 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 استفاده کنید. - صفحهٔ جدول داده — وقتی مقصد ناوبری یک صفحهٔ لیست است، صفحهبندی و مرتبسازی سرور-محور را از این الگو بردارید.