قالب صفحهٔ جزئیات (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 | عرض | کی |
|---|---|---|
default | 1200 | یک موجودیت: منشن، حساب، هشدار (پیشفرض) |
wide | 1600 | گزارشی که بخشهایش نمودار و جدول را کنار هم میگذارند: نتیجهٔ یک تحلیل، گزارش یک صفحه |
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
DetailSection
PageBackLink
PageTab
دسترسیپذیری
- پیوند بازگشت نام کامل دارد («بازگشت به منشنها»)؛ پیکان آن تزئینی است.
- زبانهها یک
navبا نام «زبانههای صفحه»اند و زبانهٔ فعلیaria-current="page"دارد؛ چون پیوندند، با Tab پیمایش میشوند و با Enter باز میشوند. - هر
DetailSectionعنواندار یک ناحیهٔ نامدار (region) است با عنوانh2.
کامپوننتهای مرتبط
قالب صفحهٔ فهرست (ListPage)
صفحهٔ یک مجموعه — منشنها، منابع، هشدارها — با سرِ صفحه، نوارابزار، فهرست، حالتها و صفحهبندی؛ همهٔ عددها و جای اقدامها در خود قالب
قالب صفحهٔ فرم (FormPage)
صفحهای که یک فرم است — ساخت یک هشدار، ویرایش یک منبع — با ردیفهای فرم در یک کارت و دکمههای انصراف و ثبت در انتهای خط