پست (Post)
یک کامپوننت برای هر پست، خبر یا بخش پخش؛ کارت (پیشفرض)، ردیف، کاشی، خلاصه و جزئیات از همان مدل SocialPost
معرفی
Post تنها کامپوننت نمایش یک پست است: پست شبکههای اجتماعی، خبر و مقالهٔ وب، و بخشی از رونوشت تلویزیون یا رادیو. دادهٔ
آن همیشه یک SocialPost است و چیدمانش با یک محور کوچک layout انتخاب میشود. پیشفرض
همهجا کارت است: Post بدون layout و فهرست پست بدون layouts کارت نمایش میدهند؛ ردیف و کاشی نماهای انتخابیاند و
فقط وقتی میآیند که خواننده با دکمهٔ چیدمان یا کد بهصراحت آنها را انتخاب کند.
| چیدمان | کجا | قاعده |
|---|---|---|
card | پیشفرض؛ خبرخوان در ستون فید | رسانه به عرض کارت، کامل و بدون برش در بازهٔ 4:5 تا 1.91:1 با حداکثر حدود 500 پیکسل ارتفاع؛ عرض کارت حداکثر 680 پیکسل |
row | انتخابی؛ فهرست برای مرور سریع | بندانگشتی 96 پیکسلی (72 در حالت فشرده) در سمت شروع، کنار متن؛ سه خط متن (دو خط فشرده) با طول خط خوانا؛ یک خط شاخص؛ ارتفاع ثابت |
tile | انتخابی؛ شبکهٔ تصویری | تنها چیدمانی که رسانه را میبُرد (1:1 یا 4:5)؛ کلیک، جزئیات را باز میکند |
summary | پستی که یک صفحهٔ تحلیل دربارهٔ آن است | بندانگشتی، هویت، دو خط متن که باز میشود، شاخصها و اقدامات همیشه پیدا |
details | صفحه یا پنل جزئیات | کل پست: متن کامل، همهٔ رسانهها بزرگ و کامل، همهٔ شاخصها و خروجی تحلیل |
وضعیتها دادهاند، نه کامپوننت: post.state (یا ویژگی state) همان پست را در حال بارگذاری، در حال پردازش، ناموفق، در
دسترس نبودن، خصوصی یا حذفشده از منبع نمایش میدهد. عددها فقط از رکورد میآیند: مقدار نامعلوم «—» است، شاخصی که برای این
مورد معنا ندارد نمایش داده نمیشود و هیچ صفری ساخته نمیشود. آمار حساب (دنبالکننده، رتبه) هیچوقت داخل پست نمیآید.
چه زمانی استفاده کنیم:
- هر جا یک پست، خبر یا بخش پخش نمایش داده میشود: فهرست نتایج جستوجو، خبرخوان، شبکهٔ تصویری، سربرگ صفحهٔ تحلیل، صفحهٔ جزئیات.
- برای فهرست،
Postرا داخلEntityCollectionبگذارید؛ ستون، اسکلت، حالت خالی و خطا، انتخاب و کیبورد را مجموعه فراهم میکند.
چه زمانی استفاده نکنیم:
- برای یک جزء تنها (فقط شاخصها یا فقط رسانه): از اجزای پایه استفاده کنید.
- کارت یا ردیف پست را خودتان از اجزای پایه نسازید؛ همین کامپوننت را با
layoutو در صورت نیاز با جایگاهها به کار ببرید.


ویدئو، 0:08


آناتومی
Post یک کامپوننت ترکیبی (compound) است: همهٔ تکهها در همین صفحه مستند شدهاند و صفحهٔ جدا ندارند. بدون children هر چیدمان ترکیب پیشفرض خودش را دارد؛ برای ترکیب دیگر، همین جایگاهها را بچینید (جدول API هر تکه در بخش Props است). تکههایی که جای دیگر هم به کار میروند کامپوننت عمومی خودشان را دارند: MediaFrame، Handle، MetricGroup و PlatformMark.

- 1
Post.Header— هویت منبع، نشانی و زمان - 2
Post.Text— متن پست با هشتگ و منشن - 3
Post.Media— رسانه در MediaFrame، بدون برش - 4
Post.Stats— شاخصها فقط از رکورد - 5
Post.Actions— اقدامهای محصول (actions)
استفاده
فهرست پستها همیشه یک EntityCollection است و هر مورد یک Post؛ ویژگیهای مورد
(item) را روی Post پخش کنید و ویژگیهای محصول را کنارش بگذارید:
'use client'
import { useRouter } from 'next/navigation'
import { Send } from 'lucide-react'
import { EntityCollection, Post, type SocialPost } from '@partodata/ui/social'
interface SearchResultsProps {
posts: SocialPost[]
loading: boolean
onAnalyze: (post: SocialPost) => void
}
export function SearchResults({ posts, loading, onAnalyze }: SearchResultsProps) {
const router = useRouter()
return (
<EntityCollection
entity="post"
items={posts}
getId={(post) => post.id}
loading={loading}
label="نتایج جستوجو"
renderItem={(post, item) => (
<Post
post={post}
{...item}
onOpen={(p) => router.push(`/posts/${p.id}`)}
metrics={['likes', 'comments', 'views']}
actions={[{ id: 'analyze', label: 'ارسال به تحلیل', icon: <Send />, onSelect: onAnalyze }]}
/>
)}
/>
)
}دادهٔ API را پیش از نمایش یک بار به مدل تبدیل کنید: ردیف PostData با fromPostData و ردیف خام جستوجوی محتوا با
fromSearchContent، هر دو از @partodata/ui/social/adapters. مجموعه چند مورد اول را با اولویت بارگذاری میکند
(priority را خودتان نگذارید).
کلیک و اقدامات
- کلیک اصلی (و Enter روی مورد فوکوسشده)
onOpenرا صدا میزند: پست را داخل محصول باز کنید (صفحه یا پنل جزئیات). - «مشاهده در منبع» یک اقدام جداست: وقتی
post.urlهست، آخرین اقدام پیوندی به منبع در برگهٔ تازه است (sourceLink={false}آن را برمیدارد). هرگز کلیک اصلی کاربر را از برنامه بیرون نمیبرد. - اقدامات محصول (ارسال به تحلیل، افزودن به گزارش، بوست) دادهاند و با
actionsمیآیند؛ سیستم طراحی هیچ اقدامی را از پیش نمیسازد. چند اقدام اول که آیکون دارند دکمهٔ آیکونیاند و بقیه در منوی «⋯». - اقدامات در
card،rowوtileبا بردن نشانگر، با فوکوس کیبورد و روی صفحهٔ لمسی پیدا میشوند و جایشان را نگه میدارند، پس با پیدا شدنشان متن جابهجا نمیشود. درsummaryوdetailsهمیشه پیدا هستند. - پیوندها، هشتگها، منشنها، دکمههای اقدام و چکباکس فقط کار خودشان را میکنند و پست را باز نمیکنند. کشیدن نشانگر برای انتخاب متن هم کلیک حساب نمیشود.
- با
selectableوonSelectedChangeچکباکس انتخاب نمایش داده میشود و Space روی مورد فوکوسشده انتخاب را عوض میکند. نام دسترسپذیر چکباکس نام نویسنده و چند واژهٔ اول متن است، تا دو پست یک نویسنده از هم جدا شوند. - ردیفِ باریکتر از 28rem (گوشی، پنل کناری) همهٔ اقدامات را در منوی «⋯» جمع میکند و آن را به انتهای خط شاخصها میبرد، تا
متن و شاخصها عرض داشته باشند.
asideدر ردیف انتهای خط سرتیتر است (حداکثر نیمی از آن و کنار تصویر 40 درصد) و ستونی کنار متن نمیگیرد. این جایگاه یکخطی است: وقتی جا کم است،asideبا «…» کوتاه میشود و نام (دستکم سه حرفپهنا) و زمان پست میمانند.
جزئیات پست همراه با نظرها
یک الگو، یک پاسخ: صفحهٔ جزئیات همان Post با layout="details" است و بحث زیر آن یک
EntityCollection با entity="comment" از Comment (رشتهٔ
thread، بدون دکمهٔ چیدمان).
مجموعه اسکلت نظرها، حالت خالی، خطا با «تلاش دوباره»، جایگاه «نظرهای بیشتر»، شمار نظرها در سربرگ و کیبورد را میدهد؛
پاسخهایی که هنوز بارگذاری نشدهاند با onLoadReplies از خود Comment میآیند. فهرست نظرها را خودتان با map نسازید.
'use client'
import { Button } from '@partodata/ui/button'
import { Comment, EntityCollection, Post, type SocialComment, type SocialPost } from '@partodata/ui/social'
interface PostDetailsProps {
post: SocialPost
comments: SocialComment[]
totalComments: number
loading: boolean
error?: unknown
onRetry: () => void
onMore?: () => void
onLoadReplies: (comment: SocialComment) => Promise<void>
}
export function PostDetails({
post,
comments,
totalComments,
loading,
error,
onRetry,
onMore,
onLoadReplies,
}: PostDetailsProps) {
return (
<div className="flex flex-col gap-6">
<Post post={post} layout="details" headingLevel={2} />
<EntityCollection
entity="comment"
items={comments}
getId={(comment) => comment.id}
label="نظرها"
header={<span>{`${totalComments} نظر`}</span>}
loading={loading}
error={error}
onRetry={onRetry}
pagination={
onMore ? (
<Button variant="ghost" size="sm" onClick={onMore}>
نظرهای بیشتر
</Button>
) : undefined
}
renderItem={(comment, item) => <Comment comment={comment} {...item} onLoadReplies={onLoadReplies} />}
/>
</div>
)
}پنل کناری جزئیات
صفحه یا پنل جزئیات همین الگو است (Post با layout="details" و EntityCollection نظرها)؛ برای پنل کناری آن را داخل
EntityDrawer یا Sheet بگذارید. (PostDetailsDrawer نسخهٔ 3 در 5.0 حذف شد.)
حالتها و انواع
خبر، بخش پخش و وضعیتها
خبر وب (kind: 'article') با تیتر، خلاصه و نویسنده نمایش داده میشود و شاخص اجتماعی ندارد؛ بخش تلویزیون یا رادیو
(kind: 'transcript') نام برنامه، زمان پخش (تاریخ و ساعت)، بازهٔ بخش و متن اطراف واژهٔ جستوجوشده را با علامت نشان میدهد.
جزئیات تشخیص متنِ بخش پخش در خط شاخصها میآید: روش بهدستآمدن متن (broadcast.textSource: «گفتار» یا «متن روی تصویر»)،
اطمینان تشخیص (confidence بین 0 و 1؛ مقدار null «—» است و نبودنش یعنی نمایش داده نمیشود) و گوینده (speaker). ردیف و
کاشی که سرتیترشان یکخطی است، بازهٔ بخش را هم در همین خط میآورند.
رسانهٔ منقضیشدهای که ابعاد ندارد، در کارت و جزئیات یک خط اطلاع است، نه کادر خالی 500 پیکسلی.
رونمایی محصول جدید در نمایشگاه با استقبال مشتریها همراه شد



| وضعیت | نمایش |
|---|---|
loading | اسکلت همان چیدمان (بدون داده هم کار میکند: <Post state="loading" />) |
processing / failed | پست با نشان «در حال پردازش» / «پردازش ناموفق بود» |
unavailable / private | پست با نشان «در دسترس نیست» / «خصوصی» |
deleted | پست با نشان «حذفشده از منبع» و پیوند «نسخهٔ بایگانی» اگر flags.archiveUrl باشد |
پرچمهای flags (تبلیغ، همکاری تجاری، محتوای حساس، سنجاقشده، ویرایششده) نشان کوچک میگیرند. پست حساس
(flags.sensitive) علاوه بر نشان، همهٔ رسانههایش را محو میکند، هر چه خود رسانهها بگویند: در کارت و جزئیات هر رسانه
دکمهٔ نمایش خودش را دارد و در ردیف، خلاصه، کاشی و تصویر پیشنمایش پیوند محو میماند (رسانهٔ کامل یک کلیک دورتر، در جزئیات
است)؛ کاشی متنیِ پست حساس با نشان «محتوای حساس» شروع میشود. سیگنالهای تحلیل فقط وقتی نمایش داده میشوند که تحلیل آنها
را ساخته باشد، و جایشان به چیدمان بستگی دارد:
| چیدمان | سیگنالها |
|---|---|
row، tile، summary | شدت «بالا» یا «فوری» و احساسِ غیرخنثی، بهصورت نشانهای کوچک در ابتدای خط شاخصها |
card، details | همهٔ سیگنالها (احساس، هیجان، موضع مخاطب، شدت، خوشه) و برچسبها با اطمینانشان |
رسانهای که در حال پردازش است یا ناموفق شده و ابعاد ندارد، در کارت و جزئیات یک کادر 16:9 با برچسب وضعیت میگیرد؛ چرخفلک اولین رسانهای را نشان میدهد که هنوز در دسترس است.
رسانه: ذخیرهشده، ذخیرهنشده، بدون رسانه
بسیاری از پستها (اینستاگرام و دیگران) در منبع رسانه دارند ولی ما آن را دریافت و ذخیره نکردهایم؛ بعضی رسانهٔ ذخیرهشده دارند؛ و بعضی (اغلب تلگرام و X) اصلاً رسانه ندارند. هر سه را صادقانه نشان دهید:
- ذخیرهشده:
media: [{ kind, src, width, height }]. - ذخیرهنشده:
media: [{ kind: 'image' | 'video', status: 'unavailable' }](برای چندرسانهای، یک مورد برای هر رسانه). همان کادر محدود کارت با نشانهٔ نوع، «رسانه ذخیره نشده است» و پیوند «مشاهده در منبع» بهpost.url؛ هیچوقت تصویر شکسته و هیچوقت شبیه بارگذاری ناموفق نیست. در ردیف فقط نشانه است. - بدون رسانه:
mediaرا نگذارید (نه آرایهای با موردunavailable). هیچ کادری نمایش داده نمیشود و کارت متنی فشرده میماند؛ فقط کاشی متن را در همان کادر کاشی میگذارد.
failed و expired یعنی رسانه را داشتیم و بارگذاری نشد یا منقضی شد. مبدلها (fromSearchContent، fromPostData) پست
اینستاگرام بدون thumbnailUrl را ذخیرهنشده و پست تلگرام یا X بدون آن را بدون رسانه میگیرند.

import { Post, type SocialPost } from '@partodata/ui/social'
const post: SocialPost = {
"id": "playground",
"source": {
"key": "instagram"
},
"kind": "photo",
"actor": {
"name": "فروشگاه نمونه",
"handle": "sample_shop"
},
"publishedAt": 1791613445524,
"url": "https://www.instagram.com/p/X1/",
"text": "جشنوارهٔ تخفیف فصلی از امروز شروع شد. #تخفیف",
"media": [
{
"kind": "image",
"status": "unavailable"
}
],
"metrics": {
"likes": 1240,
"comments": 86
}
}
export function PostExample() {
return <Post post={post} />
}ترکیب با جایگاهها
بدون children، هر چیدمان ترکیب پیشفرض خودش را دارد. برای ترکیب دیگر، جایگاهها را بچینید: Post.Select، Post.Repost،
Post.Header، Post.Text، Post.Media، Post.LinkPreview، Post.Embeds، Post.Stats، Post.Signals، Post.Tags،
Post.Actions و Post.Aside. هر بخش ترکیبهای پیشفرض جایگاه خودش را دارد و ترکیب دلخواه فقط جایگاههایی را نمایش میدهد که
چیدهاید: پستِ انتخابشدنی Post.Select لازم دارد.
| میخواهید | راه |
|---|---|
| یک مقدار محصول (وضعیت، «پست 3»، رتبه) در ترکیب پیشفرض | ویژگی aside |
| همان مقدار در ترکیب دلخواه | Post.Aside (ویژگی aside نادیده میماند) |
| ترتیب یا بخشهای دیگر | children با جایگاهها |
در محیط توسعه، اگر ترکیب دلخواهِ پست انتخابشدنی Post.Select نداشته باشد یا aside کنار children داده شود، یک هشدار
در کنسول میآید.
'use client'
import { Post, type SocialPost } from '@partodata/ui/social'
export function AnalysisSubject({ post }: { post: SocialPost }) {
return (
<Post post={post} layout="summary">
<Post.Media />
<div className="flex min-w-0 flex-1 flex-col gap-1.5">
<Post.Header />
<Post.Text maxLines={2} />
<Post.Stats keys={['likes', 'comments']} />
</div>
<Post.Aside>
<span className="text-meta">پست 3</span>
</Post.Aside>
</Post>
)
}در کامپوننت سرور <Post … /> را بدون جایگاه به کار ببرید؛ ماژولهای 'use client' در سرور قابل نقطهگذاری نیستند
(Post.Header خطا میدهد). ترکیب با جایگاهها و هر onOpen یا actions در کامپوننت کلاینت است.
راهنمای استفاده
بکنید
- فهرست تکستونی (
cardیاrow) را در ستون فید (حدود 680 پیکسل) بگذارید و شبکهٔtileرا در عرض پهن؛EntityCollectionهمین قاعده را اجرا میکند. - ابعاد رسانه را در مبدل بدهید (
mediaSize) تا کادر از ابتدا درست باشد. priorityرا فقط برای دو یا سه مورد اول بالای صفحه بگذارید.- شاخصهایی را که محصول لازم دارد با
metricsانتخاب کنید و ترتیبشان را همانجا تعیین کنید؛ فهرستِrowهمیشهmetricsبدهد (دو تا چهار شاخص). ردیف و کاشی شاخصها را در یک خط نگه میدارند و شاخصی را که جا نشود کامل کنار میگذارند. - ردیف و کارت را در ستونی با عرض معین بگذارید؛ ردیف ظرف (container) خودش است و داخل والدی که عرضش از محتوا میآید (مثل
inline-flexیا خانهٔ جدول با عرض خودکار) عرضش صفر میشود.
نکنید
- مقدار نامعلوم را با
0پر نکنید و آمار حساب را درpost.metricsنگذارید؛ مبدلها این را درست انجام میدهند. - کلیک اصلی را به منبع نفرستید؛ «مشاهده در منبع» اقدام جداست.
- برای حالت حذفشده یا در حال پردازش کامپوننت یا کارت جدا نسازید؛
stateرا تنظیم کنید. - دکمههای محصول را داخل متن یا سربرگ نسازید؛ با
actionsبدهید.
Props
Post
Post.Text / PostTextProps
Post.Media / PostMediaProps
Post.Stats / PostStatsProps
Post.Actions / PostActionsProps
Post.Select، Post.Repost، Post.Header، Post.LinkPreview، Post.Embeds، Post.Signals، Post.Tags و Post.Aside
ویژگی خاص خودشان را ندارند و ویژگیهای HTML را میپذیرند. Post.Signals در row، tile و summary مجموعهٔ کوچک (شدت بالا
یا فوری و احساسِ غیرخنثی؛ احساس خنثی نشانهای برای مرور سریع نیست) و در card و details همهٔ سیگنالها را نمایش میدهد.
EntityAction
| فیلد | نوع | توضیح |
|---|---|---|
id | string | شناسهٔ پایدار |
label | string | برچسب منو و نام دسترسپذیر دکمهٔ آیکونی |
icon | React.ReactNode | عنصر آیکون؛ اقدام بدون آیکون در منو میآید |
onSelect | (item: T) => void | اجرای اقدام برای همین مورد |
href | string | اقدام پیوندی بهجای تابع (نشانی مطلق در برگهٔ تازه) |
disabled | boolean | غیرفعال |
tone | 'default' | 'destructive' | اقدام حذفکننده |
دسترسیپذیری
- هر پست یک
articleاست که با نام نویسنده (یا شناسه، یا منبع) نامگذاری میشود؛ وقتی باز شدنی یا انتخابشدنی است فوکوس میگیرد و حلقهٔ فوکوس دیده میشود: Enter باز میکند و Space انتخاب را عوض میکند. کلیدهای جهت، Home و End به فهرست اطراف میرسند. - پیوندها، هشتگها و اقدامات کنترلهای واقعیاند و Enter و Space آنها به پست نمیرسد.
- دکمههای آیکونی نام دسترسپذیر و راهنمای شناور دارند؛ اقدامات پنهان با فوکوس کیبورد پیدا میشوند.
- پستِ در حال بارگذاری
aria-busyاست و یک «در حال بارگذاری» پنهان برای صفحهخوان دارد. - شناسهها و دامنهها جزیرهٔ چپبهراست با ارقام لاتیناند و متن کاربر جهت هر خط را از خودش میگیرد. نام، تیتر، نام برنامه،
نویسندهٔ خبر، دسته، عنوان پیشنمایش پیوند و نام بازنشرکننده هر کدام جزیرهٔ جهت خودشاناند، پس «Apple Inc.» در خط فارسی
نقطهاش را در انتها نگه میدارد؛ و همان قاعدهٔ ارقام
SocialTextرا دارند: متنی که با حرف لاتین شروع شود ارقام لاتین میگیرد («Studio 54»، نه «Studio 54»). - دکمهٔ «ترجمه» در حین ترجمه فوکوس را نگه میدارد (غیرفعال نمیشود) و جابهجایی به ترجمه یا متن اصلی را اعلام میکند.
کامپوننتهای مرتبط
- EntityCollection — فهرست و شبکهٔ پستها، با اسکلت، حالتها، انتخاب و کیبورد.
- Comment — نظرهای یک پست.
- Account — حسابی که پست را منتشر کرده است.
- مدل محتوای اجتماعی —
SocialPostو مبدلها.
شبکهٔ رسانهٔ منتشرشدهٔ اکانت
وقتی خود پست محتوای اصلی است، مانند جستوجو و شواهد، نمایش اولیه فید تکستونی کارت است. برای بخش رسانهٔ منتشرشدهٔ یک اکانت، زمینه را صریح اعلام کنید؛ این گزینه پیشفرض عمومی Post و نسبت 16:9 را تغییر نمیدهد.
<EntityCollection
entity="post"
context="account-published-media"
layouts={['tile', 'card']}
defaultLayout="tile"
items={posts}
getId={(post) => post.id}
renderItem={(post, item) => (
<Post post={post} {...item}>
<Post.Header />
<Post.Media />
<Post.Stats keys={['likes', 'comments']} wrap />
<Post.Actions />
</Post>
)}
/>Post.Stats.wrap در ردیف/کاشی اجازه میدهد همهٔ شمارشهای انتخابشده دیده شوند و از سطر مخفی حذف نشوند؛ پیشفرض آن false است. پسند و نظرِ معلوم، از جمله 0، حفظ میشوند؛ نامعلوم «—» است و متریکِ نامرتبط نمایش ندارد. context="account-published-media" فقط برای entity="post" معتبر است و بهتنهایی layout را تغییر نمیدهد.