الگوهای ریسپانسیو
راهنمای ساختاردهی رابط کاربری برای نمایش صحیح در موبایل، تبلت، و دسکتاپ
مقدمه
محصولات سوشیال لیسنینگ در دستگاههای مختلف استفاده میشوند. در پارتو UI، رویکرد پیشفرض mobile-first است: ابتدا نمایش موبایل تعریف میشود، سپس در نقاط شکست بزرگتر بازنویسی میشود.
نقاط شکست (Breakpoints)
پارتو UI از نقاط شکست استاندارد Tailwind پیروی میکند:
| نام | حداقل عرض | موارد استفاده |
|---|---|---|
| پیشفرض (موبایل) | — | نمایش موبایلاول |
sm | 640px | تبلت کوچک |
md | 768px | تبلت |
lg | 1024px | لپتاپ |
xl | 1280px | دسکتاپ |
2xl | 1536px | صفحات بزرگ |
الگوی ۱ — چیدمان شبکهای (Grid Layout)
کارتهای متریک در موبایل تکستونه، در تبلت دوستونه، و در دسکتاپ چهارستونه نمایش داده میشوند.
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
<MetricCard>
<MetricCardHeader>
<MetricCardTitle>فالوورها</MetricCardTitle>
</MetricCardHeader>
<MetricCardContent>
<MetricCardValue>۱۲۵K</MetricCardValue>
</MetricCardContent>
</MetricCard>
<MetricCard>
<MetricCardHeader>
<MetricCardTitle>تعامل</MetricCardTitle>
</MetricCardHeader>
<MetricCardContent>
<MetricCardValue>۴.۲٪</MetricCardValue>
</MetricCardContent>
</MetricCard>
{/* ... */}
</div>الگوی ۲ — سایدبار ریسپانسیو
در موبایل، سایدبار بهصورت کشویی (Sheet) نمایش داده میشود. در دسکتاپ، در کنار محتوا قرار میگیرد.
import {
SidebarProvider,
Sidebar,
SidebarTrigger,
SidebarInset,
SidebarContent,
SidebarMenu,
SidebarMenuItem,
SidebarMenuButton,
} from '@parto-system-design/ui'
import { BarChart2, Users, Settings } from 'lucide-react'
export function AppLayout({ children }: { children: React.ReactNode }) {
return (
<SidebarProvider>
<Sidebar>
<SidebarContent>
<SidebarMenu>
<SidebarMenuItem>
<SidebarMenuButton isActive>
<BarChart2 />
داشبورد
</SidebarMenuButton>
</SidebarMenuItem>
<SidebarMenuItem>
<SidebarMenuButton>
<Users />
اینفلوئنسرها
</SidebarMenuButton>
</SidebarMenuItem>
<SidebarMenuItem>
<SidebarMenuButton>
<Settings />
تنظیمات
</SidebarMenuButton>
</SidebarMenuItem>
</SidebarMenu>
</SidebarContent>
</Sidebar>
<SidebarInset>
{/* دکمه باز/بسته کردن سایدبار فقط در موبایل نمایش داده میشود */}
<header className="flex items-center gap-2 border-b p-4 md:hidden">
<SidebarTrigger />
<span className="font-semibold">پارتو</span>
</header>
<main className="flex-1 p-4 md:p-6">{children}</main>
</SidebarInset>
</SidebarProvider>
)
}الگوی ۳ — جدول داده در موبایل
جداول داده در صفحات کوچک باید با اسکرول افقی قابل مشاهده باشند.
import { DataTable, type DataTableColumn } from '@parto-system-design/ui'
// ۱. جدول با scroll افقی برای موبایل
;<div className="overflow-x-auto">
<DataTable columns={columns} data={data} />
</div>
// ۲. پنهان کردن ستونهای کماهمیت در موبایل
const columns: DataTableColumn<Row>[] = [
{ id: 'name', header: 'نام', cell: (row) => row.name },
{
id: 'followers',
header: 'فالوورها',
cell: (row) => row.followers.toLocaleString('fa-IR'),
// این ستون در موبایل پنهان میشود (با className)
className: 'hidden sm:table-cell',
},
{
id: 'engagement',
header: 'تعامل',
cell: (row) => `${row.engagement}٪`,
className: 'hidden md:table-cell',
},
{
id: 'status',
header: 'وضعیت',
cell: (row) => <Badge>{row.status}</Badge>,
},
]الگوی ۴ — کارتهای پروفایل ریسپانسیو
در موبایل، اطلاعات پروفایل بهصورت عمودی و در دسکتاپ بهصورت افقی نمایش داده میشوند.
import { Avatar, AvatarImage, AvatarFallback, Badge, SocialPlatformBadge } from '@parto-system-design/ui'
function InfluencerProfileHeader({ influencer }) {
return (
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
{/* اطلاعات اصلی */}
<div className="flex items-center gap-4">
<Avatar size="xl">
<AvatarImage src={influencer.avatar} />
<AvatarFallback>{influencer.name[0]}</AvatarFallback>
</Avatar>
<div className="flex flex-col gap-1">
<h1 className="text-xl font-bold">{influencer.name}</h1>
<div className="flex flex-wrap items-center gap-2">
<SocialPlatformBadge platform={influencer.platform} />
<Badge variant="outline">@{influencer.username}</Badge>
</div>
</div>
</div>
{/* آمار — در موبایل زیر اطلاعات، در دسکتاپ کنار */}
<div className="grid grid-cols-3 gap-4 rounded-lg border p-4 sm:flex sm:gap-8">
<div className="text-center">
<p className="text-lg font-bold">{influencer.followers.toLocaleString('fa-IR')}</p>
<p className="text-xs text-foreground-muted">فالوور</p>
</div>
<div className="text-center">
<p className="text-lg font-bold">{influencer.following.toLocaleString('fa-IR')}</p>
<p className="text-xs text-foreground-muted">دنبالشونده</p>
</div>
<div className="text-center">
<p className="text-lg font-bold">{influencer.posts.toLocaleString('fa-IR')}</p>
<p className="text-xs text-foreground-muted">پست</p>
</div>
</div>
</div>
)
}الگوی ۵ — فیلترها و جستجو در موبایل
در موبایل، فیلترها در یک Drawer نمایش داده میشوند تا فضای صفحه را اشغال نکنند.
'use client'
import { useState } from 'react'
import {
Button,
Drawer,
DrawerContent,
DrawerHeader,
DrawerTitle,
DrawerFooter,
DrawerClose,
SearchInput,
FilterChip,
} from '@parto-system-design/ui'
import { SlidersHorizontal } from 'lucide-react'
function SearchWithFilters() {
const [filtersOpen, setFiltersOpen] = useState(false)
const [activeFilters, setActiveFilters] = useState<string[]>([])
const filterOptions = [
{ label: 'اینستاگرام', value: 'instagram' },
{ label: 'تیکتاک', value: 'tiktok' },
{ label: 'میکرو اینفلوئنسر', value: 'micro' },
{ label: 'ماکرو اینفلوئنسر', value: 'macro' },
]
return (
<div className="flex flex-col gap-3">
{/* نوار جستجو + دکمه فیلتر */}
<div className="flex gap-2">
<SearchInput placeholder="جستجوی اینفلوئنسر..." className="flex-1" />
{/* دکمه فیلتر — در موبایل نمایش داده میشود */}
<Button variant="outline" size="sm" className="md:hidden" onClick={() => setFiltersOpen(true)}>
<SlidersHorizontal className="size-4" />
فیلترها
</Button>
</div>
{/* فیلترهای inline — فقط در دسکتاپ */}
<div className="hidden flex-wrap gap-2 md:flex">
{filterOptions.map((filter) => (
<FilterChip
key={filter.value}
active={activeFilters.includes(filter.value)}
onClick={() =>
setActiveFilters((prev) =>
prev.includes(filter.value) ? prev.filter((f) => f !== filter.value) : [...prev, filter.value]
)
}
>
{filter.label}
</FilterChip>
))}
</div>
{/* Drawer فیلتر برای موبایل */}
<Drawer open={filtersOpen} onOpenChange={setFiltersOpen} direction="bottom">
<DrawerContent>
<DrawerHeader>
<DrawerTitle>فیلترها</DrawerTitle>
</DrawerHeader>
<div className="flex flex-wrap gap-2 p-4">
{filterOptions.map((filter) => (
<FilterChip
key={filter.value}
active={activeFilters.includes(filter.value)}
onClick={() =>
setActiveFilters((prev) =>
prev.includes(filter.value) ? prev.filter((f) => f !== filter.value) : [...prev, filter.value]
)
}
>
{filter.label}
</FilterChip>
))}
</div>
<DrawerFooter>
<DrawerClose asChild>
<Button>اعمال فیلترها</Button>
</DrawerClose>
</DrawerFooter>
</DrawerContent>
</Drawer>
</div>
)
}الگوی ۶ — نمودارها در موبایل
نمودارها باید با ارتفاع کمتر در موبایل نمایش داده شوند تا اطلاعات اصلی قابل مشاهده باشند.
import { PartoLineChart } from '@parto-system-design/ui'
// ارتفاع نمودار بر اساس اندازه صفحه
function ResponsiveChart({ data }) {
return (
<div>
{/* موبایل: ارتفاع کمتر */}
<div className="block sm:hidden">
<PartoLineChart data={data} height={200} />
</div>
{/* تبلت و دسکتاپ: ارتفاع معمول */}
<div className="hidden sm:block">
<PartoLineChart data={data} height={350} />
</div>
</div>
)
}نکات مهم
چه چیزی باید در موبایل پنهان شود؟
| المان | موبایل | تبلت | دسکتاپ |
|---|---|---|---|
| ستونهای فرعی جدول | پنهان | نیمهپنهان | نمایش |
| سایدبار | Drawer | — | ثابت |
| فیلترهای inline | Drawer | نیمه | نمایش |
| نوار اعمال دستهای | Bottom bar | Toolbar | Toolbar |
| آمار پروفایل | عمودی | افقی | افقی |
از useIsMobile() با احتیاط استفاده کنید
این hook در سرور undefined برمیگرداند. همیشه مقدار را بررسی کنید:
import { useIsMobile } from '@parto-system-design/ui'
function MyComponent() {
const isMobile = useIsMobile()
// WRONG — ممکن است در SSR خطا بدهد
if (isMobile) { ... }
// CORRECT — undefined را در نظر بگیرید
if (isMobile === true) { ... }
if (isMobile !== false) { ... } // شامل undefined میشود
}کلاسهای RTL-safe برای responsive
// WRONG — physical
<div className="ml-4 sm:ml-0">
// CORRECT — logical (RTL-safe)
<div className="ms-4 sm:ms-0">چه زمانی از کدام الگو استفاده کنید
- جدول داده — همیشه
overflow-x-auto+ ستونهای پنهان برای موبایل - فیلترها — Drawer در موبایل، inline در دسکتاپ
- پروفایل — چیدمان عمودی در موبایل، افقی در دسکتاپ
- داشبورد — شبکه ۱/۲/۴ ستونه بر اساس breakpoint
- نمودارها — ارتفاع کمتر در موبایل