قالب صفحهٔ جزئیات (DetailPage)

صفحهٔ یک موجودیت — یک منشن، یک حساب، یک هشدار — با راه بازگشت، وضعیت، زبانه‌های نشانی‌دار، بخش‌ها و ستون کناری

معرفی

DetailPage قالب صفحهٔ یک موجودیت است: سرِ صفحه با راه بازگشت (یا مسیر صفحه)، نام موجودیت (تنها h1)، وضعیت و برچسب‌هایش، اقدام‌ها و زبانه‌ها؛ بعد بخش‌ها (DetailSection) و در صورت نیاز یک ستون کناری. عرض ثابت پیش‌فرض (1200) است و ریتم بخش‌ها از خود قالب می‌آید (48 پیکسل بین بخش‌ها).

  • راه بازگشت back پیوندی بالای عنوان است («→ منشن‌ها»، برای صفحه‌خوان «بازگشت به منشن‌ها») به صفحه‌ای که این صفحه از آن باز شده؛ breadcrumbs فقط برای صفحه‌ای دو سطح یا بیشتر عمیق، و هرگز هر دو (خطای نوع). پیوندها با linkComponent قاب محصول (مثلاً Link در Next.js) ساخته می‌شوند.
  • اقدام‌ها: یک اقدام اصلی که کاری می‌کند (onClick یا پیوند با asChild)، و اقدام‌های دیگر همه variant="default" — حذف هم؛ دکمهٔ destructive مال پنجرهٔ تأیید آن است.
  • زبانه‌ها پیوندند و هر کدام نشانی خودش را دارد؛ زبانهٔ فعلی از pathname قاب محصول پیدا می‌شود (همان قاعدهٔ منو: بخش‌های کامل مسیر، بلندترین href)، یا با activeTab.
  • ستون کناری aside از عرض 56rem محتوا کنار بخش‌هاست (عرض توکن --layout-aside-width، 320 پیکسل) و پایین‌تر از آن زیر بخش‌ها می‌آید.
  • خلاصه summary نوار عددهای کلیدی کل موجودیت زیر عنوان و بالای زبانه‌هاست (جمع‌های یک گزارش، عددهای اصلی یک حساب): دو تا شش MetricCard که قالب می‌چیند، هر کاشی دست‌کم --layout-tile-min-width (256 پیکسل)، زیر سرعنوان پنهان «خلاصه».

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

  • صفحه‌ای که یک موجودیت را با بخش‌ها یا زبانه‌ها نشان می‌دهد: یک منشن و نظرهایش، پروفایل یک حساب، جزئیات یک هشدار.
  • مقصد یک ردیف از ListPage.

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

  • ویرایش همان موجودیت در یک فرم: FormPage.
  • نمایش سریع یک ردیف بدون ترک فهرست: Sheet (پنل کناری)، نه صفحه.

استفاده

'use client'
import { Button, SentimentBadge } from '@partodata/ui'
import { Comment, EntityCollection, PlatformMark, type SocialComment } from '@partodata/ui/social'
import { DetailPage, DetailSection } from '@partodata/ui/templates'

type Mention = {
  id: string
  author: string
  text: string
  sentiment: 'positive' | 'negative' | 'neutral'
  comments: { id: string; text: string; sentiment: 'positive' | 'negative' | 'neutral' }[]
}

// The API's comment, in the social model (one Comment renders every comment).
const toComment = (c: Mention['comments'][number]): SocialComment => ({
  id: c.id,
  text: c.text,
  signals: { sentiment: c.sentiment },
})

export function MentionDetail({ mention, refer }: { mention: Mention; refer: () => void }) {
  return (
    <DetailPage
      title={mention.author}
      back={{ href: '/mentions', label: 'منشن‌ها' }}
      meta={
        <>
          <PlatformMark source="instagram" variant="badge" showLabel size="sm" />
          <SentimentBadge sentiment={mention.sentiment} size="sm" />
        </>
      }
      secondaryActions={<Button variant="default">کپی پیوند</Button>}
      primaryAction={<Button onClick={refer}>ارجاع به تحلیل‌گر</Button>}
      tabs={[
        { id: 'post', label: 'پست', href: `/mentions/${mention.id}` },
        { id: 'comments', label: 'نظرات', href: `/mentions/${mention.id}/comments`, badge: mention.comments.length },
      ]}
    >
      <DetailSection title="متن منشن">
        <p className="text-sm text-foreground">{mention.text}</p>
      </DetailSection>
      <DetailSection title="نظرهای اخیر" content="feed">
        <EntityCollection
          entity="comment"
          items={mention.comments.map(toComment)}
          getId={(comment) => comment.id}
          renderItem={(comment, item) => <Comment comment={comment} {...item} />}
        />
      </DetailSection>
    </DetailPage>
  )
}

در App Router هر زبانه یک مسیر است (/mentions/[id] و /mentions/[id]/comments). خودِ DetailPage در layout.tsx همان بخش می‌نشیند (app/mentions/[id]/layout.tsx کامپوننتی کلاینتی را رندر می‌کند که موجودیت را بار می‌کند و children را درون قالب می‌گذارد) و page.tsx هر زبانه فقط بخش‌های همان زبانه را رندر می‌کند. این‌طور با رفتن از یک زبانه به دیگری سرِ صفحه، زبانه‌ها و دادهٔ موجودیت سر جایشان می‌مانند و تمرکز روی زبانهٔ انتخاب‌شده باقی می‌ماند؛ هیچ‌وقت هر زبانه قالب خودش را نمی‌سازد. قالب شروع نمونهٔ کامل آن است.

حالت‌ها و انواع

حالت‌ها

state کل موجودیت را پوشش می‌دهد و با pageState({ data: entity, isLoading, error, onRetry }) ساخته می‌شود: در بارگذاری اسکلت بخش‌ها و در خطا ErrorState به‌جای بخش‌ها و ستون کناری می‌نشیند و سرِ صفحه (با زبانه‌ها) می‌ماند. تا نام موجودیت نرسیده، عنوان نوع موجودیت است («منشن»). بخشی که جدا بارگذاری می‌شود (نظرهای یک پست) state و skeleton خودش را روی DetailSection می‌گیرد (pageState با data خودِ فهرست و emptyCopy برای فهرست خالی؛ فهرستی با جست‌وجو یا فیلتر خودش filtered و onClearFilters را هم با هم می‌دهد): عنوان بخش می‌ماند و فقط محتوایش جایش را می‌دهد. سرعنوان حالتِ بخشِ عنوان‌دار h3 و بخشِ بی‌عنوان h2 است. موجودیتی که وجود ندارد صفحهٔ UtilityPage نوع 404 است، نه حالت خالی صفحه: وقتی درخواست تمام شد و چیزی برنگرداند (isSuccess && !data در useAsync)، همان UtilityPage به‌جای DetailPage رندر می‌شود.

بخش‌ها: DetailSection

هر بخش یک عنوان (h2)، توضیح و اقدام‌های خودش را دارد. اقدام‌های بخش ثانوی‌اند (دکمهٔ بی‌variant آن‌جا default رندر می‌شود)؛ اقدام اصلی صفحه در سرِ صفحه است.

خلاصه

summary کارت‌های MetricCard را می‌گیرد و خود قالب آن‌ها را زیر عنوان و meta، بالای زبانه‌ها، بخش‌ها و ستون کناری می‌چیند: هر تعداد کاشی که در عرض جا شود، بدون نقطهٔ شکست. عددهای خلاصه مال کل موجودیت‌اند، نه یک زبانه؛ برای همین بالای نوار زبانه‌ها می‌آیند و در همهٔ زبانه‌ها یکسان‌اند. هنگام بارگذاری یا خطای موجودیت خلاصه می‌ماند (کارت‌ها بارگذاری خودشان را نشان می‌دهند، عدد بارنشده «—»).

عددهای یک بخش (مثلاً تعامل در همین بازه، یا شاخص‌های کمّی یک بخش ارزیابی) خلاصهٔ صفحه نیستند: کاشی‌های همان DetailSection می‌مانند، روی همان ستون کاشی قالب: grid gap-layout-block-gap grid-cols-[repeat(auto-fill,minmax(min(var(--layout-tile-min-width),100%),1fr))]. یک عدد تنها جای خلاصه نیست (در description یا meta بنویسید) و شاخص‌ها با نمودار در یک بازهٔ زمانی DashboardPage است. در محیط توسعه برای کمتر از دو یا بیش از شش کاشی، یا کاشی‌ای که MetricCard نیست، هشدار می‌دهد.

<DetailPage
  title="گزارش ماهانهٔ منشن‌ها"
  back={{ href: '/mentions', label: 'منشن‌ها' }}
  summary={report.kpis.map((kpi) => (
    <MetricCard key={kpi.id}>
      <MetricCardHeader>
        <MetricCardLabel>{kpi.label}</MetricCardLabel>
      </MetricCardHeader>
      <MetricCardContent>
        <MetricCardValue>{formatNumber(kpi.value)}</MetricCardValue>
      </MetricCardContent>
    </MetricCard>
  ))}
  state={pageState({ data: report, isLoading, error, onRetry: load })}
>
  <DetailSection title="همهٔ منشن‌های دوره">…</DetailSection>
</DetailPage>

عرض، بازه و فیلترهای کل صفحه

widthعرضکی
default1200یک موجودیت: منشن، حساب، هشدار (پیش‌فرض)
wide1600گزارشی که بخش‌هایش نمودار و جدول را کنار هم می‌گذارند: نتیجهٔ یک تحلیل، گزارش یک صفحه
fullبی‌سقففقط لاگ یا جدولی که افقی پیمایش می‌شود

گزارشی که داده‌اش بازه دارد period می‌گیرد (DateRangePicker با بازه‌های آماده، بی size، اولین اقدام سرِ صفحه، مثل داشبورد) و فیلترهای کل صفحه — منبع، پلتفرم — filters را: نوارابزاری زیر سرِ صفحه (پس از زبانه‌ها)، بالای همهٔ بخش‌ها؛ با آن اقدام‌های صفحه به انتهای همان نوارابزار می‌روند. وضعیتشان وضعیت جزء است، در همان جزئی که DetailPage را رندر می‌کند (برای صفحهٔ زبانه‌دار، صفحهٔ layout.tsx بخش) و با یک context از خود محصول به صفحه‌های زبانه می‌رسد — هرگز نشانی.

شاخص‌های یک گزارش (جمع‌های یک تحلیل، قیف دریافت) همان summary بالاست (بالای زبانه‌ها؛ هنگام بارگذاری یا خطا می‌ماند). گزارشِ یک موجودیت در یک بازه DetailPage width="wide" با period است؛ DashboardPage برای کل محصول یا یک موضوع است.

بخشی با نوارابزار خودش

زبانه‌ای که یک فهرست فیلترشده است (زبانهٔ «پست‌ها»ی یک حساب) یک DetailSection با search و/یا filters خودش است: بخش نوارابزار خودش (PageToolbar: یک اندازهٔ کنترل، دکمهٔ پاک کردن تا filtered است) را بالای فهرستش می‌کشد، و فهرستش مثل فهرست یک ListPage است: state از pageState({ data: items, isLoading, error, onRetry }) (هرگز filtered آن‌جا)، فهرست خالیِ فیلترشده «نتیجه‌ای یافت نشد»، و pagination یا loadMore خود بخش زیر فهرست — هرگز صفحه‌بندی خود DataTable. toolbarEnd فقط ViewToggle یا انتخاب ستون است. اقدام اصلی ندارد (اقدام اصلی صفحه در سرِ صفحه است).

اقدام‌های چسبان و نوار پایین

در صفحهٔ بلند، با stickyActions ردیف عنوان و اقدام‌ها هنگام پیمایش در دسترس می‌ماند. bottomBar نواری چسبیده به پایین صفحه است، مثلاً برای پرسیدن از دستیار دربارهٔ همین گزارش.

<DetailPage
  title="گزارش کمپین تخفیف فصلی"
  stickyActions
  primaryAction={<Button onClick={exportReport}>خروجی گزارش</Button>}
  bottomBar={<Input aria-label="پرسش دربارهٔ گزارش" placeholder="دربارهٔ این گزارش بپرسید…" />}
>
  <DetailSection title="خلاصه">…</DetailSection>
</DetailPage>

ستون کناری

aside کارت‌هاست (Account با layout="card"، کارت چند عدد کلیدی)، نه DetailSection: عنوان بخش ندارد و بلوک‌هایش 24 پیکسل از هم فاصله دارند.

عددهای ستون کناری (مثلاً آمار پوشش یک کاربر) MetricCard یا StatDisplay در همان aside می‌مانند؛ چیدمانشان، زیر هم یا در شبکهٔ دوستونی، به عرض ستون بستگی دارد و قاعدهٔ parto/page-template آن را گزارش نمی‌کند. این عددها به summary نمی‌روند، مگر اینکه عدد کلیدی کل موجودیت باشند.

راهنمای استفاده

بکنید

  • راه بازگشت را با back به صفحهٔ فهرست بدهید؛ breadcrumbs فقط برای صفحه‌ای دو سطح یا بیشتر عمیق.
  • وضعیت و برچسب‌ها را در meta بگذارید، نه در عنوان.
  • هر زبانه را یک مسیر با نشانی خودش کنید.

نکنید

  • زبانهٔ درون محتوا (Tabs با state محلی) برای ناوبری صفحه نسازید؛ زبانه‌های صفحه نشانی دارند.
  • کارت «هیرو» با عنوان دوم (h1 دوم) نسازید؛ نام موجودیت عنوان صفحه است.
  • back و breadcrumbs را با هم ندهید.
  • عددهای کلیدی موجودیت را در بخشی به نام «خلاصه» یا «شاخص‌ها» یا مستقیم در بدنهٔ صفحه نچینید؛ آن‌ها را به summary بدهید. MetricCardها را هیچ‌جا روی تعداد ستون ثابت (grid-cols-4، md:grid-cols-2) نچینید؛ کاشی‌های یک بخش روی ستون کاشی قالب‌اند (قاعدهٔ parto/page-template هر دو را گزارش می‌کند).

Props

DetailPage

Prop

Type

DetailSection

Prop

Type

Prop

Type

PageTab

Prop

Type

دسترسی‌پذیری

  • پیوند بازگشت نام کامل دارد («بازگشت به منشن‌ها»)؛ پیکان آن تزئینی است.
  • زبانه‌ها یک nav با نام «زبانه‌های صفحه»‌اند و زبانهٔ فعلی aria-current="page" دارد؛ چون پیوندند، با Tab پیمایش می‌شوند و با Enter باز می‌شوند.
  • هر DetailSection عنوان‌دار یک ناحیهٔ نام‌دار (region) است با عنوان h2.

کامپوننت‌های مرتبط

  • ListPage — فهرستی که این صفحه یکی از ردیف‌هایش است.
  • FormPage — ویرایش همین موجودیت.
  • PageState — حالت یک بخش که جدا بارگذاری می‌شود.
  • Account با layout="card" — کارت معمول ستون کناری.