چیدمان

اجزای سطح پایینی که قالب‌های صفحه رویشان ساخته شده‌اند — PageContainer (عرض از نوع صفحه) › PageHeader › PageSection، داخل ProductFrame؛ در صفحهٔ محصول فقط PageSection، درون CustomPage

هر صفحهٔ محصول یک قالب صفحه است. قالب‌ها روی یک اسکلت ساخته شده‌اند که این صفحه اجزایش را نشان می‌دهد: PageContainer با size نوع صفحه، سپس سربرگ صفحه (PageHeader، تنها <h1> صفحه)، سپس PageSectionها. عرض، حاشیه، فاصلهٔ بالای عنوان و ریتم بخش‌ها را همین اجزا می‌دهند و هیچ کدی عدد فاصله یا عرضی نمی‌نویسد. مرجع کامل props و اجزا در صفحهٔ کامپوننت داربست صفحه (Page*) است.

صفحهٔ محصول با قالب ساخته می‌شود

هر صفحهٔ محصول یکی از قالب‌های صفحه است (ListPage، DetailPage، FormPage، SettingsPage، DashboardPage، UtilityPage) که همین اسکلت را خودش می‌سازد: عرض، سرِ صفحه، ریتم، جای اقدام‌ها و حالت‌ها. این صفحه اجزای سطح پایینی را نشان می‌دهد که قالب‌ها روی آن‌ها ساخته شده‌اند؛ مستقیم فقط درون CustomPage به کارشان ببرید.

همهٔ صفحه‌ها داخل ProductFrame

این الگوها صفحه‌هایی را نشان می‌دهند که داخل قاب محصول، ProductFrame، رندر می‌شوند. ناحیهٔ محتوای قاب padding و سقف عرض ندارد، پس عرض و حاشیه را قالب صفحه یک بار می‌دهد؛ قاب یا ظرف دیگری دور صفحه نسازید.

درون قالب‌ها: یک اسکلت، یک جای سربرگ

هر قالب سربرگش را (PageHeader تخت) اولین فرزند PageContainer خودش رندر می‌کند؛ صفحهٔ محصول هیچ‌کدام را نمی‌نویسد و زبانه، متا و راه بازگشت را از propهای قالب می‌دهد (DetailPage: tabs، meta، back). سربرگ ترکیبی PageHeaderRoot جزء سطح پایین کد خود سیستم طراحی است؛ اجزای آن داخل PageContainer عرض و حاشیهٔ همان ظرف را می‌گیرند و ظرف دومی نمی‌سازند.


نمونه بصری


پیاده‌سازی

صفحهٔ محصول این اسکلت را خودش نمی‌نویسد: قالبش آن را می‌سازد. تنها جایی که اجزای این صفحه مستقیم به کار می‌روند محتوای یک CustomPage است — صفحه‌ای که در هیچ قالبی جا نمی‌شود و DS-GAP ثبت‌شده دارد. سرِ صفحه، عرض و ریتم را همان قالب می‌دهد و محتوا PageSectionهای شماست:

import { Button, PageSection } from '@partodata/ui'
import { Icons } from '@partodata/ui/icons'
import { CustomPage } from '@partodata/ui/templates'

declare function createBulletin(): void

// app/reports/campaign/page.tsx — قاب را layout ریشه یک بار می‌دهد؛ صفحه فقط قالب خودش را برمی‌گرداند.
export default function CampaignReportPage() {
  return (
    <CustomPage
      dsGap="DS-GAP-15: گزارش ترکیبی کمپین"
      title="گزارش کمپین تخفیف فصلی"
      description="نمای کلی عملکرد کمپین در شبکه‌های اجتماعی"
      secondaryActions={<Button variant="default">اشتراک‌گذاری</Button>}
      primaryAction={
        <Button onClick={createBulletin} iconStart={<Icons.plus />}>
          ساخت بولتن
        </Button>
      }
    >
      <PageSection>{/* بخش اول */}</PageSection>
      <PageSection>{/* بخش دوم */}</PageSection>
    </CustomPage>
  )
}
  • عرض: نامی است که قالب می‌گیرد (width: narrow 768 · default 1200 · wide 1600 · full)، و هر قالب عرض نوع صفحه‌اش را خودش دارد (هندسهٔ صفحه).
  • اقدام اصلی: هر صفحه یک اقدام اصلی دارد و یک جا برای آن: primaryAction قالب، که همیشه آخر رندر می‌شود؛ قالب تصمیم می‌گیرد در سرِ صفحه باشد یا در انتهای نوارابزار. اقدام‌های دیگر در secondaryActions، هر کدام variant="default".

الگوهای رایج

هر نوع صفحه قالب خودش را دارد و عرض، سرِ صفحه، ریتم، جای اقدام‌ها و حالت‌هایش را همان قالب تعیین می‌کند. قالب را با انتخاب قالب صفحه انتخاب کنید؛ نمونه‌های کامل و کامپایل‌شده در صفحهٔ هر قالب است.

تنظیمات

گروه‌هایی از تنظیمات که هر کدام جدا ذخیره می‌شوند SettingsPage است با یک SettingsSection برای هر گروه (نوار ذخیرهٔ خودش را دارد)؛ صفحه‌ای که فقط یک «ذخیره» و یک «انصراف» دارد، حتی اگر نامش «تنظیمات …» باشد، FormPage است. عرض هر دو باریک (768) است و سرِ صفحه اقدامی ندارد.

<SettingsPage title="تنظیمات حساب" description="اطلاعات پروفایل و تنظیمات امنیتی">
  <SettingsSection
    title="اطلاعات پروفایل"
    onSubmit={form.handleSubmit(save)}
    onCancel={() => form.reset()}
    dirty={form.formState.isDirty}
  >
    {/* FormRowها */}
  </SettingsSection>
</SettingsPage>

لیست

صفحهٔ لیست ListPage است: جست‌وجو، فیلترها و اقدام‌ها propهای قالب‌اند و نوارابزار را خود قالب می‌سازد؛ جدول یا فهرست فرزند آن است و حالت‌ها و صفحه‌بندی propهایش. اقدام اصلی با جست‌وجو یا فیلتر در انتهای نوارابزار و بی آن‌ها در انتهای سرِ صفحه است — قالب جایش را تعیین می‌کند. نسخهٔ زنده: صفحهٔ «منشن‌ها» در قالب شروع.

بلوک آماده: قالب شروع (Starter)

کد و نمای کامل
<ListPage
  title="اینفلوئنسرها"
  search={
    <SearchInput
      placeholder="جست‌وجو در اینفلوئنسرها"
      aria-label="جست‌وجو در اینفلوئنسرها"
      value={q}
      onChange={(e) => filterBy(setQ)(e.target.value)}
      onClear={() => filterBy(setQ)('')}
    />
  }
  filtered={q !== ''}
  onClearFilters={clear}
  primaryAction={
    <Button asChild iconStart={<Icons.plus />}>
      <Link href="/influencers/new">افزودن اینفلوئنسر</Link>
    </Button>
  }
  state={pageState({
    data: data?.items,
    isLoading,
    error,
    onRetry: load,
    emptyCopy: { title: 'هنوز اینفلوئنسری ثبت نشده است' },
  })}
  pagination={{ currentPage: page, totalPages, onPageChange: setPage, totalRows, pageSize: 25 }}
>
  <DataTable columns={columns} data={data?.items ?? []} />
</ListPage>

داشبورد

داشبورد سوشال لیسنینگ DashboardPage است با عرض پهن (1600): انتخابگر دوره در period، شاخص‌ها در kpis و هر ردیف نمودار یک DashboardSection از DashboardChartها. انتخابگر دوره DateRangePicker با بازه‌های آماده است و صفحهٔ تازه با «ماه گذشته» شروع می‌شود. نسخهٔ کامل در ترکیب داشبورد آمده است.

<DashboardPage
  title="داشبورد سوشال لیسنینگ"
  period={<DateRangePicker value={period} onChange={setPeriod} />}
  kpis={metrics}
>
  <DashboardSection title="روند گفت‌وگو">
    <DashboardChart title="روند منشن‌ها">{/* نمودار */}</DashboardChart>
  </DashboardSection>
</DashboardPage>

صفحات جزئیات

صفحهٔ یک موجودیت (پروفایل یک اینفلوئنسر، یک منشن) DetailPage است با عرض پیش‌فرض (1200): راه بازگشت back به صفحه‌ای که از آن باز شده، ستون کناری در aside و هر بخش یک DetailSection.

<DetailPage
  title="علی احمدی"
  back={{ href: '/influencers', label: 'اینفلوئنسرها' }}
  primaryAction={<Button onClick={follow}>دنبال کردن</Button>}
  aside={<Account account={influencer} layout="card" />}
>
  <DetailSection title="نرخ تعامل">{/* EngagementRate */}</DetailSection>
  <DetailSection title="احساسات مخاطبان">{/* SentimentDistribution */}</DetailSection>
</DetailPage>

نسخهٔ زنده: بلاک پروفایل اینفلوئنسر.

بلوک آماده: پروفایل اینفلوئنسر

کد و نمای کامل

کامپوننت‌ها

PageContainer

کانتینر اصلی که عرض حداکثر و padding افقی یکنواخت را بر اساس variant اندازه می‌دهد و context @container است.

<PageContainer size="small" | "default" | "large" | "full">{/* محتوا */}</PageContainer>

size را نوع صفحه تعیین می‌کند، نه سلیقهٔ صفحه: small فرم، تنظیمات و صفحه‌های کمکی · default فهرست و جزئیات · large داشبورد، خبرخوان و تحلیل · full فقط جدول بسیار پهن یا لاگ. عرض هر پله و توکنش در هندسهٔ صفحه آمده است.

PageHeaderRoot

سربرگ ترکیبی (آیکون، ردیف متا، تب‌های ناوبری با PageHeaderNavigationTabs)، جزء سطح پایینی برای کد خود سیستم طراحی؛ صفحهٔ محصول آن را نمی‌نویسد — حتی در CustomPage — و صفحه‌ای که زبانه یا متا در سرش لازم دارد DetailPage است. درون یک PageContainer عرض و حاشیهٔ همان ظرف را می‌گیرد؛ size را به آن ندهید.

// مرجع جزء، برای کد خود سیستم طراحی — صفحهٔ محصول این را نمی‌نویسد.
<PageHeaderRoot>
  <PageHeaderMeta>
    <PageHeaderIcon>{/* آیکون موجودیت */}</PageHeaderIcon>
    <PageHeaderSummary>
      <PageHeaderTitle>عنوان صفحه</PageHeaderTitle>
      <PageHeaderDescription>توضیح صفحه</PageHeaderDescription>
    </PageHeaderSummary>
    <PageHeaderAside>
      <Button>اقدام دیگر</Button>
      <Button variant="primary" onClick={act}>
        اقدام اصلی
      </Button>
    </PageHeaderAside>
  </PageHeaderMeta>
  <PageHeaderNavigationTabs>{/* تب‌ها */}</PageHeaderNavigationTabs>
</PageHeaderRoot>

در PageHeaderAside اقدام اصلی آخرین دکمه است و تنها دکمهٔ variant="primary" (از 4.0 دکمهٔ بی‌variant خنثی است). شکل پیش از 4.0 — PageHeaderRoot پیش از PageContainer و هم‌سطح آن — هنوز همان‌طور رندر می‌شود (هر جزء خودش را در PageContainer هم‌اندازهٔ size می‌پیچد).

PageSection

کامپوننت ترکیبی برای سازماندهی محتوای صفحه به بخش‌های متمایز. عنوان/توضیح در PageSectionSummary (داخل PageSectionMeta) و بدنه در PageSectionContent می‌رود.

<PageSection orientation="vertical" | "horizontal">
  <PageSectionMeta>
    <PageSectionSummary>
      <PageSectionTitle>عنوان بخش</PageSectionTitle>
      <PageSectionDescription>توضیح بخش</PageSectionDescription>
    </PageSectionSummary>
    <PageSectionAside>
      <Button variant="default">اقدام بخش</Button>
    </PageSectionAside>
  </PageSectionMeta>
  <PageSectionContent>{/* محتوای اصلی — فرم، جدول، گرید */}</PageSectionContent>
</PageSection>

محتوای اصلی بخش کجا می‌رود؟

محتوای اصلی بخش در PageSectionContent می‌رود؛ بخش‌ها اسلات جداگانه‌ای برای محتوای «اصلی» ندارند، و PageSectionAside یک خوشهٔ اقدامات است نه یک ستون. برای چیدمان دو ستونی از orientation="horizontal" استفاده کنید (ستون اول PageSectionMeta، ستون دوم PageSectionContent).


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

  • هر صفحه یک قالب: صفحهٔ محصول یکی از قالب‌های صفحه است و این اجزا را خودش نمی‌سازد. از اجزای این صفحه، PageSectionها محتوای یک CustomPageاند و PageContainer و سربرگ فقط در کد خود سیستم طراحی به کار می‌روند؛ آن‌جا هم فاصله‌گذاری دستی نگذارید و به آن‌ها کلاس فاصله یا عرض ندهید.
  • سربرگ را قالب می‌دهد: صفحه PageContainer، PageHeader یا PageHeaderRoot نمی‌نویسد (قاعدهٔ ESLint parto/page-template)؛ درون قالب‌ها سربرگ اولین فرزند PageContainer آن‌هاست.
  • یک اقدام اصلی: در primaryAction قالب صفحه؛ هیچ‌وقت در نوار بالای قاب.
  • محتوای اصلی در PageSectionContent: بخش‌ها اسلات محتوای «اصلی» جداگانه ندارند — همهٔ محتوا در PageSectionContent می‌رود.
  • RTL: همهٔ کامپوننت‌ها از CSS Logical Properties استفاده می‌کنند و خودکار RTL-صحیح‌اند.
  • واکنش‌گرایی: چیدمان‌ها container-query محورند و در عرض‌های مختلف درست رفتار می‌کنند.

صفحات مرتبط

  • داربست صفحه (Page*) — مرجع کامل props و اجزای این خانواده
  • PageHeader — سری که قالب‌های صفحه رندر می‌کنند
  • ListPage — صفحهٔ فهرست: جست‌وجو، فیلتر و اقدام‌هایش propهای قالب‌اند (PageToolbar جزء سطح پایین آن است، فقط درون CustomPage)
  • صفحهٔ تحلیل — نمونهٔ کامل صفحهٔ تحلیل یک اینفلوئنسر روی DetailPage
  • ترکیب داشبورد — الگوی چیدمان داشبورد