سایدبار فیلتر (FilterPanel)

سایدبار فیلتر در ستون دوم قاب، با همه‌چیز داخلش — شمار فیلترهای فعال و «پاک کردن همه»، چیپ‌های مقدار فعال، نشانگر هر بخش و کنترل متناسب با داده؛ تنها جای دیگر فیلتر، چند فیلتر کشویی در نوار ابزار است

معرفی

فیلترهای یک صفحه فقط در یکی از دو جا هستند (تصمیم مالک، 9 اکتبر 2026):

جاکیچطور
(الف) سایدبار فیلتر5 بُعد یا بیشتر، بُعدهایی که کنار هر مقدار شمار دارند، یا صفحه‌ای که کار اصلی‌اش فیلتر است (جست‌وجو، کاوش)filterPanel={{ panel, activeCount }} در ListPage، DashboardPage و CustomPage
(ب) چند فیلتر کشویی در نوار ابزارحداکثر 4 بُعد که گاه‌به‌گاه عوض می‌شوند، بالای فهرستfilters در ListPage

FilterPanel همان سایدبار است. قالب آن را به ستون دوم ProductFrame، کنار منو، می‌برد و همه‌چیزِ فیلترها داخل همان ستون است:

  1. سربرگ «N فیلتر فعال · پاک کردن همه» (FilterPanelTitle activeCount و FilterPanelClearAll)
  2. چیپ‌های مقدار فعال، درست زیر سربرگ (FilterPanelActiveFilters؛ قالب آن را از activeFilters می‌کشد)
  3. یک FilterSection برای هر بُعد، با نشانگر فعال خودش (قرص شمار برند، خلاصهٔ مقدار وقتی بسته است، «پاک کردن») و کنترلی که با نوع داده جور است

نوار ابزار فقط جست‌وجو، مرتب‌سازی و اقدام‌ها را دارد: نه دکمهٔ «فیلترها»، نه چیپ، نه «پاک کردن فیلترها». الگوی کار صفحهٔ Logs در Supabase Studio است: بازهٔ زمانی اول و باز، بُعدهای پرتکرار باز، کم‌کاربردها بسته.

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

  • صفحهٔ جست‌وجو، کاوش یا تحلیل یک نتیجهٔ بزرگ با 5 بُعد یا بیشتر
  • بُعدهایی که کنار هر مقدار شمار نتیجه دارند (استان، موضوع، احساس)
  • داشبوردی که فیلترهای زیاد یا متنوع دارد (DashboardPage filterPanel)

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

  • صفحهٔ فهرست یا جدولی با حداکثر 4 بُعد: filters قالب ListPage (فیلترهای کشویی نوار ابزار). صفحهٔ جدولی با 2 بُعد یا کمتر هرگز سایدبار نمی‌گیرد (ESLint parto/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.

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

آناتومی سایدبار

همان ستون بیرون از قاب، با همه‌چیز داخلش: شمار و «پاک کردن همه»، چیپ‌ها، بازهٔ زمانی با بازه‌های آماده و فیلد بازهٔ دلخواه، احساس با مربع رنگی و «فقط»، هیجان با چیپ رنگی و آیکون، و استانِ بسته با خلاصهٔ مقدار فعال.

فیلترهافیلترهای فعال3

فیلترهای فعال
منفیخشمتهران

در برابر 7 روز پیش از آن

  • ترتیب بخش‌ها از تصمیم‌های پرتکرار صفحه می‌آید، نه الفبا: بازهٔ زمانی اول و باز، بُعدهای پرتکرار باز، کم‌کاربردها با 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

Prop

Type

FilterPanelActiveFilters

مقدارهای فعال به‌صورت FilterChip حذف‌شدنی، بالای بدنهٔ پنل و زیر سربرگ. در ListPage و DashboardPage قالب خودش آن را از activeFilters می‌کشد؛ در CustomPage یا بیرون از قالب خودتان آن را اول FilterPanelBody بگذارید.

Prop

Type

FilterPanelTitle

Prop

Type

FilterPanelClearAll

Prop

Type

FilterSection

Prop

Type

FilterSectionOptions

Prop

Type

FilterPanelTrigger

فقط ماشهٔ Sheet موبایل یا محتوای خیلی باریک؛ قالب‌ها خودشان آن را می‌گذارند. هرگز دکمهٔ نوار ابزار دسکتاپ.

Prop

Type

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

  • سایدبار یک <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)