پرتوپرتو

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

پوستهٔ رسمی و آماده برای ساخت داشبوردها و پنل‌های مدیریت — نوار ناوبری، هدر و بوم محتوا در یک کامپوننت

معرفی

AppShell پوستهٔ رسمی و کانونیکال برنامه‌های پارتو است؛ همان چیدمان استودیو‌مانند (نوار ناوبری تیره در لبه، هدر چسبان و بوم محتوا) که پیش‌تر هر اپلیکیشن آن را دستی سرِ‌هم می‌کرد، حالا در یک کامپوننت ترکیب‌شده است. کافی است ناوبری، هدر و محتوای صفحه را به‌صورت اعلانی توصیف کنید تا کل کروم برنامه به‌شکل یکسان و درست ساخته شود.

چه زمانی استفاده کنیم

  • برنامه‌ای چند‌صفحه‌ای با نوار ناوبری ثابت، هدر بالای صفحه و بوم محتوای اسکرول‌شونده دارید.
  • می‌خواهید همهٔ اپلیکیشن‌های مصرف‌کننده، پوسته و ریتم بصری یکسان (فاصله‌گذاری، بوم، عرض محتوا) داشته باشند.

چه زمانی استفاده نکنیم

  • صفحهٔ تک و بدون ناوبری اصلی دارید → فقط از PageHeader داخل چیدمان خودتان استفاده کنید.
  • به کنترل کاملاً سفارشی روی هر لایه نیاز دارید → مستقیماً از خانوادهٔ اولیهٔ NavRail استفاده کنید؛ AppShell همان‌ها را با پیش‌فرض‌های درست می‌بندد.

کانونیکال

AppShell به‌همراه خانوادهٔ NavRail/SiteHeader/PageHeader روش پیشنهادی و رسمی ساخت پوستهٔ برنامه است. الگوی قدیمی مبتنی بر Sidebar + AppBar همچنان کار می‌کند اما «جایگزین قدیمی» محسوب می‌شود (پایین همین صفحه).

نمونه بصری

پایش برند

نمای کلی گفت‌وگوهای شبکه‌های اجتماعی

داشبورد

خلاصه وضعیت برند در هفته گذشته

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

۱٬۲۳۴
۱۲.۵٪+

نرخ تعامل

۴.۷٪
۲.۱٪-

منشن‌ها

۸۵۶
۲۳.۴٪+

بازدید

۲.۱ میلیون
۸.۷٪+

روند تعامل

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

این هفته
#برند_ما
#محصول_جدید
#کمپین_تخفیف
#رضایت_مشتری

پیاده‌سازی

اسکلت آماده و کپی‌پیست‌شدنی. ناوبری را به‌صورت آرایه بدهید، آیتم فعال را با activeId مشخص کنید و جابه‌جایی را با onNavigate مدیریت کنید.

'use client'

import * as React from 'react'
import { AppShell, PageHeader, Button, Card, CardHeader, CardTitle, CardContent } from '@partodata/ui'
import { LayoutDashboard, Users, FileBarChart } from 'lucide-react'

const NAV = [
  { id: 'dashboard', label: 'داشبورد', icon: <LayoutDashboard /> },
  { id: 'influencers', label: 'اینفلوئنسرها', icon: <Users /> },
  { id: 'reports', label: 'گزارش‌ها', icon: <FileBarChart /> },
]

export function App() {
  const [view, setView] = React.useState('dashboard')

  return (
    <AppShell
      brand={
        <div className="flex size-9 items-center justify-center rounded-md bg-brand-default text-sm font-bold text-on-brand">
          پ
        </div>
      }
      nav={NAV}
      activeId={view}
      onNavigate={setView}
      header={{
        title: 'پایش برند',
        subtitle: 'نمای کلی گفت‌وگوهای شبکه‌های اجتماعی',
        actions: <Button size="sm">گزارش جدید</Button>,
      }}
      pageHeader={<PageHeader title="داشبورد" description="خلاصه وضعیت برند در هفته گذشته" />}
    >
      <Card>
        <CardHeader>
          <CardTitle>روند تعامل</CardTitle>
        </CardHeader>
        <CardContent>{/* محتوای صفحه اینجا قرار می‌گیرد */}</CardContent>
      </Card>
    </AppShell>
  )
}

RTL

در چیدمان راست‌به‌چپ، navSide="start" (پیش‌فرض) نوار ناوبری را به لبهٔ راست می‌چسباند. همهٔ فاصله‌گذاری‌ها با خصوصیت‌های منطقی (ps/pe/start/end) پیاده شده‌اند و در هر دو تم روشن و تیره درست کار می‌کنند.

دستور آشپزی بوم (تک‌تُن — Single-Tone Chrome)

AppShell دقیقاً چیدمان تک‌تُن استودیوی Supabase را می‌سازد: هدر، نوار ناوبری، پنل جانبی و بوم محتوا همگی روی یک تُن (--background-sidebar) می‌نشینند و فقط با خط ۱ پیکسلی border-default از هم جدا می‌شوند. عمق رابط از کارت‌ها می‌آید، نه از اختلاف تُن کروم.

لایهنقشتوکن پس‌زمینه
کروم (هدر + نوار + پنل جانبی + بوم)بدنهٔ برنامه، تک‌تُنbg-dash-sidebar (--background-sidebar)
کارت‌ها و سطوحسطح روشن‌تر که محتوا را جدا می‌کندsurface-100/surface-200 (خود کارت می‌آورد)
جدول‌های دادهسطح تیره‌ترِ مخصوص گرید داده--background-canvas (رزرو‌شده)

بوم به‌صورت خودکار روی bg-dash-sidebar تنظیم می‌شود؛ کافی است محتوا را با Card/MetricCard بچینید تا خودشان سطح surface-100 را بیاورند. پس‌زمینهٔ صفحه را دستی روشن نکنید — بگذارید کروم تک‌تُن بماند و کارت‌ها بالا بیایند. تُن تیره‌تر --background-canvas فقط برای سطوح گرید داده رزرو شده است.

ریتم عرض و فاصله‌گذاری

محتوا در یک ستون مرکزی با فاصله‌گذاری استاندارد قرار می‌گیرد (mx-auto w-full max-w-7xl px-4 sm:px-6 lg:px-8 py-6). عرض این ستون با contentWidth کنترل می‌شود:

<AppShell contentWidth="default" ...>  {/* max-w-7xl — پیش‌فرض، داشبوردها */}
<AppShell contentWidth="narrow" ...>   {/* max-w-3xl — ستون خواندنی، فرم‌ها و تنظیمات */}
<AppShell contentWidth="full" ...>     {/* بدون محدودیت — جدول‌ها و بوم‌های تمام‌عرض */}

الگوهای رایج

شل کمینه با رفتار ریل قابل‌کنترل

ساده‌ترین ترکیب AppShell: حالت contained (برای نشاندن داخل هر ظرف با ارتفاع مشخص) به‌همراه کنترل navBehavior/onNavBehaviorChange — کاربر می‌تواند ریل را باز، جمع، یا شناور کند و انتخابش حفظ شود:

پایش برند

نمای ساده

خلاصه

سایدبار دوبخشی: پنل جانبی ماندگار (secondaryPanel)

این همان «Part 2» استودیو است: برای فیلترها، فید فعالیت یا ساب‌ناوبری که باید در کل مسیر باقی بماند، secondaryPanel را بدهید؛ AppShell یک پنل ۲۵۶ پیکسلی کنار نوار رزرو می‌کند. داخلش از خانوادهٔ SecondaryNav استفاده کنید. با secondaryResizable قابل تغییر‌عرض (۲۵۶ تا ۵۱۲px) می‌شود.

<AppShell
  nav={NAV}
  activeId={view}
  onNavigate={setView}
  header={{ title: 'تحلیل‌ها' }}
  secondaryResizable
  secondaryPanel={
    <>
      <AppSecondaryHeader>
        <AppSecondaryTitle>گزارش‌ها</AppSecondaryTitle>
      </AppSecondaryHeader>
      <AppSecondaryContent className="gap-1 p-2">
        <SecondaryNavSearchInput placeholder="جستجوی گزارش…" aria-label="جستجوی گزارش" />
        <SecondaryNavTitle>پیشنهادی</SecondaryNavTitle>
        <SecondaryNavItem isActive>نمای کلی هفتگی</SecondaryNavItem>
        <SecondaryNavItem>روند تعامل</SecondaryNavItem>
      </AppSecondaryContent>
    </>
  }
>
  {page}
</AppShell>

حالت‌های نوار ناوبری (navBehavior)

نوار به‌طور پیش‌فرض جمع (۴۸px) است و با اشاره‌گر باز می‌شود. کاربر می‌تواند از کنترل پاورقی نوار حالت را عوض کند (در localStorage ذخیره می‌شود)؛ مقدار اولیه را با navBehavior بدهید:

<AppShell navBehavior="open" ... >   {/* همیشه باز — چیدمان ۲۰۸px را رزرو می‌کند */}
<AppShell navBehavior="expandable" ... /> {/* پیش‌فرض — جمع، با hover باز */}
<AppShell navBehavior="closed" ... />  {/* همیشه جمع */}

برای حالت کنترل‌شده (مثلاً پیش‌نمایش/embed که نباید در localStorage مشترک ذخیره شود)، onNavBehaviorChange را هم بدهید؛ در این حالت هیچ چیزی ذخیره یا خوانده نمی‌شود:

const [behavior, setBehavior] = React.useState<NavRailBehavior>('open')

<AppShell navBehavior={behavior} onNavBehaviorChange={setBehavior} ... >

پوستهٔ ساده (فقط نوار)

اگر secondaryPanel ندهید، پوسته به حالت سادهٔ «فقط نوار» درمی‌آید — برای محصولات ساده‌تر:

<AppShell nav={NAV} activeId={view} onNavigate={setView} header={{ title: 'پایش برند' }}>
  {page}
</AppShell>

جایگزینی اسلات‌های هدر

اگر به چیدمان سفارشی در هدر نیاز دارید، به‌جای header.actions می‌توانید کل اسلات ابتدا/انتها را با headerStart/headerEnd جایگزین کنید (دکمهٔ منوی موبایل همیشه حفظ می‌شود).

<AppShell
  nav={NAV}
  headerEnd={
    <>
      <SearchBox />
      <ThemeToggle />
      <UserMenu />
    </>
  }
>
  {page}
</AppShell>

جاسازی در یک جعبهٔ محدود (contained)

به‌طور پیش‌فرض پوسته کل نمای مرورگر (viewport) را می‌گیرد. برای جاسازی در یک جعبهٔ محدود (مثلاً پیش‌نمایش یا نمایش داخل صفحه)، contained را روشن کنید تا نوار ناوبری ثابت به همان جعبه مقید شود و بوم درون خودش اسکرول شود.

<div className="h-[560px] overflow-hidden rounded-lg border">
  <AppShell contained nav={NAV} activeId={view} onNavigate={setView} header={{ title: 'پیش‌نمایش' }}>
    {page}
  </AppShell>
</div>

داشبورد: کارت‌های KPI + جدول

محتوای صفحه (فرزندان AppShell) را با گریدهای استاندارد بچینید:

<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 xl:grid-cols-4">
  <MetricCard>
    <MetricCardHeader>
      <MetricCardLabel>مجموع منشن‌ها</MetricCardLabel>
    </MetricCardHeader>
    <MetricCardContent>
      <MetricCardValue>۱٬۲۳۴</MetricCardValue>
      <MetricCardDifferential variant="positive" sign="+">
        ۱۲.۵٪
      </MetricCardDifferential>
    </MetricCardContent>
  </MetricCard>
  {/* سایر KPIها */}
</div>

جدول ویژگی‌ها

AppShell

ویژگینوعپیش‌فرضتوضیح
navAppShellNavItem[]مقصدهای ناوبری اصلی (آیکون + برچسب + شناسه)
activeIdstringشناسهٔ آیتم فعال ناوبری
onNavigate(id, item) => voidفراخوانی هنگام کلیک روی آیتم؛ در حالت ناوبری خالص، preventDefault باعث می‌شود پنل شناور باز نشود
header{ title?, subtitle?, actions?, breadcrumbs? }پیکربندی اعلانی هدر چسبان بالای صفحه
brandReactNodeنشان برند در سرِ نوار ناوبری (لوگوی فشرده)
navFooterReactNodeمحتوای پایین نوار ناوبری (تنظیمات، حساب کاربری)
secondaryPanelReactNodeپنل جانبی ماندگار (Part 2) کنار نوار؛ نبودنش ⇒ پوستهٔ سادهٔ فقط‌نوار
secondaryResizablebooleanfalseاجازهٔ تغییر‌عرض پنل جانبی با کشیدن (۲۵۶ تا ۵۱۲px)
navBehavior"expandable" | "open" | "closed""expandable"حالت نوار؛ بدون onNavBehaviorChange مقدار اولیه (uncontrolled)، با آن کاملاً کنترل‌شده
onNavBehaviorChange(behavior) => voidکنترل‌شده‌کردن navBehavior — انتخاب کاربر از کنترل پاورقی را دریافت می‌کند (بدون ذخیره‌سازی)
hideNavBehaviorTogglebooleanfalseپنهان‌کردن کنترل حالت در پاورقی نوار
pageHeaderReactNodeناحیهٔ PageHeader در بالای بوم، بالای فرزندان
headerStartReactNodeجایگزینی کامل اسلات ابتدای هدر
headerEndReactNodeجایگزینی کامل اسلات انتهای هدر
headerSize"sm" | "md" | "lg""md"ارتفاع هدر (md = ۴۸px)
navSide"start" | "end""start"لبهٔ اتصال نوار ناوبری (RTL: start = راست)
contentWidth"default" | "narrow" | "full""default"عرض ستون محتوا (max-w-7xl / max-w-3xl / max-w-full)
containedbooleanfalseمقید‌کردن پوسته به جعبهٔ والد به‌جای نمای مرورگر

بهترین روش‌ها + دام‌های رایج

بکنید

  • بگذارید کروم تک‌تُن (bg-dash-sidebar) بماند و کارت‌ها با surface-100 بالا بیایند — «عمق» از کارت‌ها می‌آید نه از تفاوت تُن کروم. - عرض محتوا را فقط با contentWidth تعیین کنید (نام‌های default/narrow/full)، نه با کلاس max-w دلخواه. - برای ناوبری، onNavigate را بدهید؛ کلیک روی آیتم مستقیماً همان را صدا می‌زند.

نکنید

  • پس‌زمینهٔ صفحه یا بوم را دستی روشن (bg-background) نکنید — کروم باید تک‌تُن بماند و کارت‌ها بالا بیایند. - تُن --background-canvas را برای کروم به کار نبرید؛ آن فقط برای سطوح گرید داده رزرو است. - max-w سفارشی روی محتوا نگذارید — ریتم عرض را بین اپ‌ها ناهماهنگ می‌کند.

دام overflow

اگر محتوای عریض (جدول، نمودار) بوم را از هم می‌کِشد، مشکل از min-w-0 است. AppShell خودش min-w-0 را روی فرزند فلکسِ محتوا اعمال می‌کند؛ اگر داخل صفحه چیدمان فلکس تودرتو دارید، min-w-0 را روی فرزندهای درونی هم تکرار کنید.

رویکرد جایگزین (قدیمی): Sidebar + AppBar

پیش از AppShell، پوسته با SidebarProvider/Sidebar و AppBar دستی ساخته می‌شد. این الگو همچنان پشتیبانی می‌شود، اما فقط زمانی سراغش بروید که به سایدبارِ بازشونده با گروه‌های متنی (نه نوار آیکونی باریک) نیاز دارید؛ برای پوستهٔ استاندارد استودیو‌مانند، AppShell انتخاب درست است.

import {
  SidebarProvider,
  Sidebar,
  SidebarContent,
  SidebarHeader,
  SidebarMenu,
  SidebarMenuItem,
  SidebarMenuButton,
  SidebarInset,
  SidebarTrigger,
  AppBar,
} from '@partodata/ui'
import { LayoutDashboard } from 'lucide-react'

function LegacyShell({ children }: { children: React.ReactNode }) {
  return (
    <SidebarProvider>
      <Sidebar>
        <SidebarHeader>
          <span className="px-2 font-bold text-foreground">پارتو</span>
        </SidebarHeader>
        <SidebarContent>
          <SidebarMenu>
            <SidebarMenuItem>
              <SidebarMenuButton isActive>
                <LayoutDashboard className="size-4" />
                <span>داشبورد</span>
              </SidebarMenuButton>
            </SidebarMenuItem>
          </SidebarMenu>
        </SidebarContent>
      </Sidebar>
      <SidebarInset>
        <AppBar logo={<SidebarTrigger />} />
        <div className="p-6">{children}</div>
      </SidebarInset>
    </SidebarProvider>
  )
}

قوانین 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]
فرم تنظیماتcontentWidth="narrow"

صفحات مرتبط

  • NavRail — خانوادهٔ اولیهٔ نوار ناوبری؛ وقتی به کنترل کامل روی هر لایه نیاز دارید.
  • SiteHeader — هدر چسبان برنامه؛ AppShell آن را با header می‌بندد.
  • PageHeader — سرصفحهٔ محتوا؛ برای صفحات تک بدون پوستهٔ کامل.
  • ترکیب داشبورد — چیدمان محتوای داخل بوم.