پوسته برنامه (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
| ویژگی | نوع | پیشفرض | توضیح |
|---|---|---|---|
nav | AppShellNavItem[] | — | مقصدهای ناوبری اصلی (آیکون + برچسب + شناسه) |
activeId | string | — | شناسهٔ آیتم فعال ناوبری |
onNavigate | (id, item) => void | — | فراخوانی هنگام کلیک روی آیتم؛ در حالت ناوبری خالص، preventDefault باعث میشود پنل شناور باز نشود |
header | { title?, subtitle?, actions?, breadcrumbs? } | — | پیکربندی اعلانی هدر چسبان بالای صفحه |
brand | ReactNode | — | نشان برند در سرِ نوار ناوبری (لوگوی فشرده) |
navFooter | ReactNode | — | محتوای پایین نوار ناوبری (تنظیمات، حساب کاربری) |
secondaryPanel | ReactNode | — | پنل جانبی ماندگار (Part 2) کنار نوار؛ نبودنش ⇒ پوستهٔ سادهٔ فقطنوار |
secondaryResizable | boolean | false | اجازهٔ تغییرعرض پنل جانبی با کشیدن (۲۵۶ تا ۵۱۲px) |
navBehavior | "expandable" | "open" | "closed" | "expandable" | حالت نوار؛ بدون onNavBehaviorChange مقدار اولیه (uncontrolled)، با آن کاملاً کنترلشده |
onNavBehaviorChange | (behavior) => void | — | کنترلشدهکردن navBehavior — انتخاب کاربر از کنترل پاورقی را دریافت میکند (بدون ذخیرهسازی) |
hideNavBehaviorToggle | boolean | false | پنهانکردن کنترل حالت در پاورقی نوار |
pageHeader | ReactNode | — | ناحیهٔ PageHeader در بالای بوم، بالای فرزندان |
headerStart | ReactNode | — | جایگزینی کامل اسلات ابتدای هدر |
headerEnd | ReactNode | — | جایگزینی کامل اسلات انتهای هدر |
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) |
contained | boolean | false | مقیدکردن پوسته به جعبهٔ والد بهجای نمای مرورگر |
بهترین روشها + دامهای رایج
بکنید
- بگذارید کروم تکتُن (
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 Cards | grid-cols-1 sm:grid-cols-2 xl:grid-cols-4 |
| Card Grid | grid-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— سرصفحهٔ محتوا؛ برای صفحات تک بدون پوستهٔ کامل.- ترکیب داشبورد — چیدمان محتوای داخل بوم.