پرتوپرتو

پوسته برنامه (AppShell)

الگوی ترکیب Sidebar + Header + Content برای ساخت داشبوردها و پنل‌های مدیریت

معرفی

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

نمونه بصری

مح

داشبورد

خلاصه وضعیت برند در شبکه‌های اجتماعی

اینفلوئنسرها

۱,۲۳۴
۱۲.۵٪+

نرخ تعامل

۴.۷٪
۲.۱٪-

اشاره‌ها

۸۵۶
۲۳.۴٪+

بازدید

۲.۱M
۸.۷٪+

روند تعامل

هشتگ‌های برتر

این هفته
#برند_ما
#محصول_جدید
#نظرسنجی
#رضایت_مشتری

ساختار پایه

┌──────────────────────────────────────────┐
│               Header (sticky)             │
├────────────┬─────────────────────────────┤
│            │                              │
│  Sidebar   │       Main Content           │
│  (ثابت)    │       (scrollable)           │
│            │                              │
│            │                              │
└────────────┴─────────────────────────────┘

پیاده‌سازی با DS

import {
  AppBar,
  Avatar,
  AvatarFallback,
  Button,
  Sidebar,
  SidebarContent,
  SidebarGroup,
  SidebarGroupLabel,
  SidebarGroupContent,
  SidebarHeader,
  SidebarMenu,
  SidebarMenuItem,
  SidebarMenuButton,
  SidebarProvider,
  SidebarInset,
  SidebarTrigger,
  PageHeader,
} from '@parto-system-design/ui'
import { Bell, LayoutDashboard, Users, BarChart3, Settings } from 'lucide-react'

function AppShell({ children }: { children: React.ReactNode }) {
  return (
    <SidebarProvider>
      <Sidebar>
        <SidebarHeader>
          <span className="font-bold text-foreground px-2">پارتو</span>
        </SidebarHeader>
        <SidebarContent>
          <SidebarGroup>
            <SidebarGroupLabel>اصلی</SidebarGroupLabel>
            <SidebarGroupContent>
              <SidebarMenu>
                <SidebarMenuItem>
                  <SidebarMenuButton asChild>
                    <a href="/">
                      <LayoutDashboard className="size-4" />
                      <span>داشبورد</span>
                    </a>
                  </SidebarMenuButton>
                </SidebarMenuItem>
                <SidebarMenuItem>
                  <SidebarMenuButton asChild>
                    <a href="/influencers">
                      <Users className="size-4" />
                      <span>اینفلوئنسرها</span>
                    </a>
                  </SidebarMenuButton>
                </SidebarMenuItem>
                <SidebarMenuItem>
                  <SidebarMenuButton asChild>
                    <a href="/analytics">
                      <BarChart3 className="size-4" />
                      <span>تحلیل‌ها</span>
                    </a>
                  </SidebarMenuButton>
                </SidebarMenuItem>
                <SidebarMenuItem>
                  <SidebarMenuButton asChild>
                    <a href="/settings">
                      <Settings className="size-4" />
                      <span>تنظیمات</span>
                    </a>
                  </SidebarMenuButton>
                </SidebarMenuItem>
              </SidebarMenu>
            </SidebarGroupContent>
          </SidebarGroup>
        </SidebarContent>
      </Sidebar>

      <SidebarInset>
        {/* AppBar — نوار بالای صفحه */}
        <AppBar
          logo={<SidebarTrigger />}
          actions={
            <Button variant="ghost" size="icon">
              <Bell className="size-4" />
            </Button>
          }
          user={
            <Avatar>
              <AvatarFallback>م</AvatarFallback>
            </Avatar>
          }
        />

        {/* Page Content */}
        <div className="p-6">{children}</div>
      </SidebarInset>
    </SidebarProvider>
  )
}

نکته

SidebarTrigger را به‌عنوان logo در AppBar قرار دهید. این دکمه سایدبار را باز و بسته می‌کند و با Ctrl+B هم کار می‌کند.

الگوی داشبورد

KPI + جدول

import {
  MetricCard,
  MetricCardHeader,
  MetricCardLabel,
  MetricCardContent,
  MetricCardValue,
  MetricCardDifferential,
  DataTable,
  PageHeader,
  PageHeaderTitle,
  PageHeaderAside,
  Button,
} from '@parto-system-design/ui'

function DashboardPage() {
  return (
    <>
      <PageHeader>
        <PageHeaderTitle>داشبورد</PageHeaderTitle>
        <PageHeaderAside>
          <Button size="sm">گزارش جدید</Button>
        </PageHeaderAside>
      </PageHeader>

      {/* KPI Cards */}
      <div className="grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-4 gap-4 mb-6">
        <MetricCard>
          <MetricCardHeader>
            <MetricCardLabel>مجموع تحلیل‌ها</MetricCardLabel>
          </MetricCardHeader>
          <MetricCardContent>
            <MetricCardValue>۱,۲۳۴</MetricCardValue>
            <MetricCardDifferential value={12.5} />
          </MetricCardContent>
        </MetricCard>
        {/* سایر KPI‌ها */}
      </div>

      {/* Data Table */}
      <DataTable columns={columns} data={data} />
    </>
  )
}

فیلتر + گرید کارت

import {
  FilterBar,
  ViewToggle,
  Card,
  CardHeader,
  CardTitle,
  CardContent,
  Empty,
  EmptyIcon,
  EmptyTitle,
  EmptyDescription,
} from '@parto-system-design/ui'

function ListPage() {
  const [view, setView] = useState<'grid' | 'list'>('grid')

  return (
    <>
      <div className="flex items-center justify-between gap-4 mb-4">
        <FilterBar filters={filters} onFilterChange={setFilters} />
        <ViewToggle view={view} onViewChange={setView} />
      </div>

      {data.length === 0 ? (
        <Empty>
          <EmptyIcon>
            <Icons.inbox />
          </EmptyIcon>
          <EmptyTitle>موردی یافت نشد</EmptyTitle>
          <EmptyDescription>فیلترها را تغییر دهید یا مورد جدیدی اضافه کنید</EmptyDescription>
        </Empty>
      ) : (
        <div
          className={cn('gap-4', view === 'grid' ? 'grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3' : 'flex flex-col')}
        >
          {data.map((item) => (
            <Card key={item.id} variant="interactive">
              <CardHeader>
                <CardTitle>{item.title}</CardTitle>
              </CardHeader>
              <CardContent>{/* ... */}</CardContent>
            </Card>
          ))}
        </div>
      )}
    </>
  )
}

الگوی موبایل

Drawer به‌عنوان Bottom Sheet

برای فیلترها و اقدامات موبایل، از Drawer با direction="bottom" استفاده کنید:

import {
  Drawer,
  DrawerTrigger,
  DrawerContent,
  DrawerHeader,
  DrawerTitle,
  DrawerFooter,
  Button,
} from '@parto-system-design/ui'

function MobileFilters() {
  return (
    <Drawer direction="bottom">
      <DrawerTrigger asChild>
        <Button variant="outline" size="sm" className="lg:hidden">
          فیلترها
        </Button>
      </DrawerTrigger>
      <DrawerContent>
        <DrawerHeader>
          <DrawerTitle>فیلترها</DrawerTitle>
        </DrawerHeader>
        <div className="p-4">{/* فرم فیلترها */}</div>
        <DrawerFooter>
          <Button>اعمال</Button>
        </DrawerFooter>
      </DrawerContent>
    </Drawer>
  )
}

الگوی صفحات طولانی

SectionNavigator

برای صفحات تحلیلی با بخش‌های متعدد:

import { SectionNavigator } from '@parto-system-design/ui'

function AnalysisPage() {
  const sections = [
    { id: 'overview', label: 'خلاصه' },
    { id: 'engagement', label: 'تعامل' },
    { id: 'content', label: 'محتوا' },
    { id: 'audience', label: 'مخاطبان' },
  ]

  return (
    <div className="grid grid-cols-1 lg:grid-cols-[240px_1fr] gap-6">
      <aside className="hidden lg:block sticky top-20 h-fit">
        <SectionNavigator sections={sections} offset={80} />
      </aside>

      <div>
        <section id="overview">{/* ... */}</section>
        <section id="engagement">{/* ... */}</section>
        <section id="content">{/* ... */}</section>
        <section id="audience">{/* ... */}</section>
      </div>
    </div>
  )
}

الگوی خطا و بارگذاری

Error Boundary + Loading

import { ErrorBoundary, PageLoader } from '@parto-system-design/ui'

function App() {
  return (
    <ErrorBoundary
      fallback={({ error, reset }) => (
        <div className="p-8 text-center">
          <p className="text-destructive mb-4">{error.message}</p>
          <Button onClick={reset}>تلاش مجدد</Button>
        </div>
      )}
    >
      <Suspense fallback={<PageLoader message="در حال بارگذاری..." />}>
        <MyPage />
      </Suspense>
    </ErrorBoundary>
  )
}

قوانین Grid

صفحهGrid پیشنهادی
KPI Cardsgrid-cols-1 sm:grid-cols-2 xl:grid-cols-4
Card Gridgrid-cols-1 md:grid-cols-2 lg:grid-cols-3
داشبورد ۲ ستونیgrid-cols-1 lg:grid-cols-[2fr_1fr]
Analysis + Sidebargrid-cols-1 lg:grid-cols-[240px_1fr]
فرم تنظیماتmax-w-2xl mx-auto