سایدبار فیلتر (FilterPanel)
سایدبار فیلتر در ستون دوم قاب، با همهچیز داخلش — شمار فیلترهای فعال و «پاک کردن همه»، چیپهای مقدار فعال، نشانگر هر بخش و کنترل متناسب با داده؛ تنها جای دیگر فیلتر، چند فیلتر کشویی در نوار ابزار است
معرفی
فیلترهای یک صفحه فقط در یکی از دو جا هستند (تصمیم مالک، 9 اکتبر 2026):
| جا | کی | چطور |
|---|---|---|
| (الف) سایدبار فیلتر | 5 بُعد یا بیشتر، بُعدهایی که کنار هر مقدار شمار دارند، یا صفحهای که کار اصلیاش فیلتر است (جستوجو، کاوش) | filterPanel={{ panel, activeCount }} در ListPage، DashboardPage و CustomPage |
| (ب) چند فیلتر کشویی در نوار ابزار | حداکثر 4 بُعد که گاهبهگاه عوض میشوند، بالای فهرست | filters در ListPage |
FilterPanel همان سایدبار است. قالب آن را به ستون دوم ProductFrame، کنار منو،
میبرد و همهچیزِ فیلترها داخل همان ستون است:
- سربرگ «N فیلتر فعال · پاک کردن همه» (
FilterPanelTitle activeCountوFilterPanelClearAll) - چیپهای مقدار فعال، درست زیر سربرگ (
FilterPanelActiveFilters؛ قالب آن را ازactiveFiltersمیکشد) - یک
FilterSectionبرای هر بُعد، با نشانگر فعال خودش (قرص شمار برند، خلاصهٔ مقدار وقتی بسته است، «پاک کردن») و کنترلی که با نوع داده جور است
نوار ابزار فقط جستوجو، مرتبسازی و اقدامها را دارد: نه دکمهٔ «فیلترها»، نه چیپ، نه «پاک کردن فیلترها». الگوی کار صفحهٔ Logs در Supabase Studio است: بازهٔ زمانی اول و باز، بُعدهای پرتکرار باز، کمکاربردها بسته.
چه زمانی استفاده کنیم:
- صفحهٔ جستوجو، کاوش یا تحلیل یک نتیجهٔ بزرگ با 5 بُعد یا بیشتر
- بُعدهایی که کنار هر مقدار شمار نتیجه دارند (استان، موضوع، احساس)
- داشبوردی که فیلترهای زیاد یا متنوع دارد (
DashboardPage filterPanel)
چه زمانی استفاده نکنیم:
- صفحهٔ فهرست یا جدولی با حداکثر 4 بُعد:
filtersقالبListPage(فیلترهای کشویی نوار ابزار). صفحهٔ جدولی با 2 بُعد یا کمتر هرگز سایدبار نمیگیرد (ESLintparto/filter-placement) - پنلی کنار نتیجهها با دکمهٔ «فیلترها» در نوار ابزار: منسوخ (پایین همین صفحه)
- چیپهای فیلتر فعال بالای نتیجهها: چیپها داخل خود سایدبارند
وقتی ستون دوم آزاد نیست
ستون دوم در هر صفحه یا ناوبری بخش است یا فیلتر، هرگز هر دو. اگر secondaryNav همان صفحه ستون را گرفته باشد، قاب
نباشد (chrome="none") یا کنار ستون کمتر از 40rem برای محتوا بماند، همان بخشها نوار افقی بالای فهرست میشوند: هر
FilterSection یک Popover با همان کنترلها، و ماشهٔ هر کدام مقدار خودش را نشان میدهد. پس ردیف چیپ جدا نیست. زیر 36rem
محتوا (و روی موبایل) همان پنل در Sheet باز میشود. ستون سوم فیلتر یا فیلتر زیر ناوبری ساخته نمیشود.
استفاده
وضعیت فیلترها مال صفحه است (React.useState). هر تغییر فوراً اعمال میشود و فهرست از اول بار میشود: فوتر «اعمال»
ندارد.
'use client'
import * as React from 'react'
import {
DateRangePicker,
FilterChip,
FilterPanel,
FilterPanelBody,
FilterPanelClearAll,
FilterPanelHeader,
FilterPanelTitle,
FilterSection,
FilterSectionChoices,
FilterSectionOptions,
FilterSectionPeriod,
resolveDateRangePreset,
type DateRangeValue,
} from '@partodata/ui'
import { ListPage } from '@partodata/ui/templates'
const PERIODS = [
{ value: '7d', label: '7 روز' },
{ value: '30d', label: '30 روز' },
{ value: '90d', label: '90 روز' },
]
const EMOTIONS = [
{ value: 'anger', label: 'خشم', tone: 'emotion:anger' as const, count: 96 },
{ value: 'joy', label: 'شادی', tone: 'emotion:joy' as const, count: 140 },
{ value: 'trust', label: 'اعتماد', tone: 'emotion:trust' as const, count: 88 },
]
const PROVINCES = [
{ value: 'tehran', label: 'تهران', count: 486 },
{ value: 'isfahan', label: 'اصفهان', count: 173 },
{ value: 'fars', label: 'فارس', count: 98 },
]
export function MentionSearch() {
const [range, setRange] = React.useState<DateRangeValue | undefined>(() => resolveDateRangePreset('30d'))
const [emotions, setEmotions] = React.useState<string[]>([])
const [provinces, setProvinces] = React.useState<string[]>([])
const periodActive = range?.preset !== '30d'
const activeCount = emotions.length + provinces.length + (periodActive ? 1 : 0)
const clear = () => {
setRange(resolveDateRangePreset('30d'))
setEmotions([])
setProvinces([])
}
const panel = (
<FilterPanel>
<FilterPanelHeader>
<FilterPanelTitle activeCount={activeCount} />
{activeCount > 0 && <FilterPanelClearAll onClear={clear} />}
</FilterPanelHeader>
<FilterPanelBody>
<FilterSectionPeriod
title="بازهٔ زمانی"
presets={PERIODS}
defaultValue="30d"
value={range?.preset}
onValueChange={(key) => setRange(resolveDateRangePreset(key))}
picker={<DateRangePicker value={range} onChange={setRange} relativeInput />}
comparison="در برابر 30 روز پیش از آن"
/>
<FilterSection
title="هیجان غالب"
activeCount={emotions.length}
onClear={emotions.length ? () => setEmotions([]) : undefined}
>
<FilterSectionChoices variant="chips" options={EMOTIONS} value={emotions} onValueChange={setEmotions} />
</FilterSection>
<FilterSection
title="استان"
defaultOpen={false}
activeCount={provinces.length}
summary={provinces.length ? `${provinces.length} استان` : undefined}
onClear={provinces.length ? () => setProvinces([]) : undefined}
>
<FilterSectionOptions options={PROVINCES} value={provinces} onValueChange={setProvinces} />
</FilterSection>
</FilterPanelBody>
</FilterPanel>
)
return (
<ListPage
title="جستوجوی منشنها"
filterPanel={{ panel, activeCount }}
// Drawn at the top of the sidebar, under «N فیلتر فعال · پاک کردن همه» — never in the toolbar.
activeFilters={emotions.map((value) => (
<FilterChip
key={value}
label={EMOTIONS.find((e) => e.value === value)?.label ?? value}
onRemove={() => setEmotions((all) => all.filter((x) => x !== value))}
/>
))}
filtered={activeCount > 0}
onClearFilters={clear}
>
{/* the results */}
</ListPage>
)
}نمونهٔ کامل و قابلکامپایل (پنج بُعد، شمار هر مقدار، فهرست با اسکرول خودکار):
npx --no parto-ui example list-page-filter-panel.
حالتها و انواع
آناتومی سایدبار
همان ستون بیرون از قاب، با همهچیز داخلش: شمار و «پاک کردن همه»، چیپها، بازهٔ زمانی با بازههای آماده و فیلد بازهٔ دلخواه، احساس با مربع رنگی و «فقط»، هیجان با چیپ رنگی و آیکون، و استانِ بسته با خلاصهٔ مقدار فعال.
- ترتیب بخشها از تصمیمهای پرتکرار صفحه میآید، نه الفبا: بازهٔ زمانی اول و باز، بُعدهای پرتکرار باز،
کمکاربردها با
defaultOpen={false}بسته و باsummary(مقدار فعال در چند کلمه، زیر عنوان بخش بسته). - کنترل متناسب با داده، نه «همهچیز فهرست چکباکس»:
| داده | کنترل |
|---|---|
| بازهٔ زمانی | FilterSectionPeriod: چیپ بازههای آماده، تقویم با relativeInput، و خط دورهٔ مقایسه |
| دستهٔ کوتاه با معنای خودش (هیجان، تازگی) | FilterSectionChoices variant="chips" با tone |
| چند مقدار با نشانهٔ قوی (منبع، زبان) | FilterSectionChoices variant="cards" |
| دیدگاه مخاطب با سهم | FilterSectionChoices variant="swatches" |
| سطح بحران با شمار | FilterSectionChoices variant="levelCards" |
| عدد دارای ترتیب | FilterSectionRange |
| فهرست بلند اسمی (استان، موضوع) یا مقدار با شمار مقایسهای | FilterSectionOptions |
| بله/خیر | Switch |
FilterSectionOptions: فهرست مقدارها
مثل فهرستهای صفحهٔ Logs در Supabase: یک جعبهٔ مرزدار، ردیفها با خط مویی جدا، و اسکرول داخلی بعد از 10 ردیف.
- هر ردیف 32 پیکسل است و کل ردیف کلیکپذیر است: کنترل، مربع رنگ یا آیکون، برچسب، و شمار در انتهای ردیف.
- «فقط»: با hover یا فوکوس صفحهکلید، اقدام «فقط» جای شمار را میگیرد و همان مقدار را بهتنهایی انتخاب میکند. یک
کلیک بهجای برداشتن ده تیک. نام دسترسیپذیرش «فقط تهران» است. در چندانتخابی با بیش از 2 مقدار روشن است
(
only={false}خاموشش میکند). tone: مربع 10 پیکسلی رنگ معنایی مقدار (sentiment:*،emotion:*،flow:*،status:*،severity:*)، همان رنگ نشان و نمودار. کلمه همیشه میماند.- بیش از 8 مقدار: فیلد جستوجو («جستوجو در استان»؛ ی/ي و ک/ك یکیاند). بیش از
limit(پیشفرض 6): «N مورد دیگر»؛ مقدار انتخابشده همیشه دیده میشود. - شمار هر مقدار پیش از اعمال همین بُعد حساب میشود؛ صفر کمرنگ است ولی پنهان نمیشود.
loading: هنگام بارگذاری شمارها، سه ردیف اسکلت در همان جعبه.
بازهٔ زمانی
FilterSectionPeriod بازههای پرتکرار را چیپ یککلیکی میکند (به سبک Google Analytics) و «دلخواه» تقویم را باز
میکند. با relativeInput روی DateRangePicker، بالای بازههای آماده فیلدی هست که هر بازهای تا امروز را با تایپ
میپذیرد (مثل فیلد «2h، 30m، 7d» در Supabase): «45 روز»، «45d»، «2 هفته» یا «3 ماه». Enter همان را اعمال میکند. مقدار
کلید '45d' را نگه میدارد و نسبی میماند، پس در نشانی صفحه یا نمای ذخیرهشده هم درست بازخوانی میشود
(resolveDateRangePreset('45d')). بازهٔ پیشفرض صفحه فیلتر فعال حساب نمیشود.
ستون جمعشدنی (اختیاری)
collapsible پیشفرض خاموش است و در Prototype هم خاموش میماند: سایدبار فیلتر همیشه هست. محصولی که عرض نتیجهها
برایش مهم است میتواند آن را روشن کند. آنوقت FilterPanelHeader دکمهٔ «بستن فیلترها» میگیرد و Mod+B ستون را باز و
بسته میکند. ستونِ بسته یک نوار 40 پیکسلی در قاب میگذارد با زبانهٔ «فیلترها» و شمار فیلتر فعال، تا ستون از همانجا
برگردد. هرگز دکمهای در نوار ابزار.
موبایل: Sheet
روی موبایل همان پنل در Sheet باز میشود و ماشهاش FilterPanelTrigger است، تنها جای مجاز این دکمه. قالبها خودشان
آن را میگذارند.
نکتهٔ دسترسیپذیری
Sheet روی Radix Dialog سوار است و یک SheetTitle لازم دارد. اگر عنوان در FilterPanelHeader دیده میشود، یک
<SheetTitle className="sr-only"> بگذارید تا تکرار بصری نشود.
بخشها
این بخش بهطور پیشفرض باز است. روی عنوان کلیک کنید تا جمع شود.
برای فیلترهای تکفیلدی که همیشه قابلمشاهده باشند
- پیشفرض: جمعشدنی با شِورون،
defaultOpen={true} defaultOpen={false}: در رندر اول بسته است (بُعد کمکاربرد)activeCount: قرص پر برند کنار عنوان، در حالت بسته هم. همراه باonClear، اقدام متنی «پاک کردن» در انتهای سر بخشsummary: مقدار فعال در چند کلمه، زیر عنوان بخش بستهdefaultActive: فقط پیشفرض سامانه در این بخش فعال است: نشان کمصدای «پیشفرض» بهجای قرصcollapsible={false}: سر ثابت، برای فیلدی که همیشه باید دیده شود
منسوخها (7.13، حذف در 8.0)
| منسوخ | بهجای آن |
|---|---|
filterPanel.placement: 'page': پنل کنار نتیجهها با دکمهٔ «فیلترها» | placement را بردارید: سایدبار فیلتر در ستون قاب |
FilterPanelLayout: همان الگو برای صفحهٔ سفارشی | CustomPage filterPanel={{ panel, activeCount }} |
FilterPanelTrigger در نوار ابزار دسکتاپ | هیچ: سایدبار همیشه دیده میشود. ستون جمعشده از زبانهٔ خودش در قاب برمیگردد |
FilterBarActiveFilters بالای نتیجهها کنار پنل | activeFilters قالب (داخل پنل رسم میشود) یا FilterPanelActiveFilters |
فوتر «اعمال» (FilterPanelFooter) در سایدبار | هر تغییر فوراً اعمال میشود |
قالبها در حالت توسعه برای هر مورد یک بار هشدار میدهند و ESLint parto/filter-placement آن را در کد نشان میدهد. جدول
کامل مهاجرت در MIGRATION-v7.md، بخش «7.13: یک جای فیلتر».
راهنمای استفاده
بکنید
- فیلترهای زیاد را به
filterPanelقالب بدهید وplacementرا ننویسید: پنل در ستون دوم قاب مینشیند - همهٔ وضعیت فیلترها را داخل سایدبار نشان دهید: «N فیلتر فعال · پاک کردن همه»، چیپها (activeFilters) و نشانگر هر بخش - بازهٔ زمانی را اول و باز بگذارید، بُعدهای پرتکرار را باز و کمکاربردها را باdefaultOpen={false}وsummary- کنترل هر بخش را از نوع داده انتخاب کنید (FilterSectionChoices،FilterSectionRange،FilterSectionOptions) - وضعیت را در صفحه نگه دارید (React.useState) و با هر تغییر فهرست را از اول بار کنید
نکنید
- پنل را کنار نتیجهها نگذارید و دکمهٔ «فیلترها» را در نوار ابزار دسکتاپ نگذارید - چیپ فیلتر فعال را بیرون از سایدبار
نشان ندهید - فوتر «اعمال» نگذارید - ردیف چکباکس را دستی نسازید:
FilterSectionOptions- همهٔ بخشها را فهرست چکباکس نکنید -filtersوfilterPanelرا با هم ندهید - برای 1 یا 2 فیلتر سایدبار نسازید:filtersقالب کافی است
Props
FilterPanel
FilterPanelActiveFilters
مقدارهای فعال بهصورت FilterChip حذفشدنی، بالای بدنهٔ پنل و زیر سربرگ. در ListPage و DashboardPage قالب خودش آن
را از activeFilters میکشد؛ در CustomPage یا بیرون از قالب خودتان آن را اول FilterPanelBody بگذارید.
FilterPanelTitle
FilterPanelClearAll
FilterSection
FilterSectionOptions
FilterPanelTrigger
فقط ماشهٔ Sheet موبایل یا محتوای خیلی باریک؛ قالبها خودشان آن را میگذارند. هرگز دکمهٔ نوار ابزار دسکتاپ.
دسترسیپذیری
- سایدبار یک
<aside>با نام «فیلترها» است؛ سربرگh2است و شمار فعال را بهصورت متن (با برچسبsr-only«فیلترهای فعال») نگه میدارد، پس بخش بسته هم شمار فعالش را اعلام میکند FilterSectionروی@radix-ui/react-collapsibleاست:aria-expandedخودکار، Space و Enter برای باز و بستن- فهرست مقدارها یک
groupبا نام عنوان بخش است و هر ردیف یک چکباکس واقعی با برچسب کامل؛ «فقط» یک دکمهٔ جدا با نام «فقط» بههمراه نام همان مقدار (مثلاً «فقط تهران») است و با Tab در دسترس است (با فوکوس دیده میشود) - مربع رنگ
aria-hiddenاست و کلمه همیشه کنارش میماند؛ رنگ هرگز تنها معنا را نمیرساند - شِورون بخش بسته جهت سند را دنبال میکند (در RTL به چپ)
- زبانهٔ ستون جمعشده
aria-expanded="false"،aria-controlsبه پنل وaria-keyshortcuts="Control+B Meta+B"دارد و شمار فعال را میخواند؛ با باز شدن، فوکوس به دکمهٔ «بستن فیلترها» در سر پنل میرود - انیمیشن باز و بسته با
prefers-reduced-motionخاموش میشود
کامپوننتهای مرتبط
- ListPage: برای حداکثر 4 فیلتر ساده،
filters(کشوییهای نوار ابزار) کافی است؛ سایدبار برای بیشتر از آن است - FilterSectionChoices: وقتی داده معنای بصری دارد (هیجان، زبان، سطح)، بهجای فهرست چکباکس
- FilterSectionRange: برای عدد دارای ترتیب، نه چهار گزینهٔ رادیویی
- DateRangePicker: تقویم بازهٔ زمانی با بازههای آماده و
relativeInput - FilterChip: چیپ مقدار فعال، داخل سایدبار
- ProductFrame: ستون دوم و عرض پهن آن (
filterSidebarWidth="wide"، 18rem)
دیالوگ ویرایش برچسب (LabelEditDialog)
دیالوگ کنترلشده برای ویرایش برچسبهای خوشهبندی هوشمند — با فیلد متنی، تاگل «تولیدشده توسط AI»، پیشنهادهای انتخاب سریع و اکشن حذف اختیاری.
فیلتر بازهٔ عددی (FilterSectionRange)
بخش فیلتر برای عددی که ترتیب دارد (امتیاز اعتماد، سن اکانت، روز از آخرین ورود) — دو فیلد «از» و «تا» با اعتبارسنجی لحظهای، میانبرهای شمارشدار و خلاصهٔ مقدار فعال در بخش بسته.