پرتوپرتو

پنل فیلتر (FilterPanel)

پنل فیلتر composable برای داشبوردهای تحلیل — بخش‌های قابل‌جمع‌شدن، شمارنده‌ی فعال، حذف به تفکیک، و ActiveFiltersBar جداگانه

معرفی

FilterPanel اسکلت composable یک پنل فیلتر است — هدر با شمارنده و دکمه‌ی «پاک کردن همه»، بدنه‌ی اسکرول‌دار با FilterSectionهای قابل‌جمع‌شدن، و فوتر چسبان برای دکمه‌های اعمال/لغو. خودش state را مدیریت نمی‌کند؛ فرم‌ها و state را مصرف‌کننده می‌چیند — همین باعث می‌شود هم به‌صورت سایدبار همیشگی داخل یک <aside> کار کند و هم داخل یک Sheet به‌صورت slide-in.

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

  • داشبوردهای افکارسنجی/سوشیال لیستنینگ که ۴+ بُعد فیلتر دارند (بازه زمانی، پلتفرم، احساس، جریان، کلیدواژه، …)
  • صفحاتی که کاربر چندین فیلتر را هم‌زمان می‌چرخاند و باید یک دید کلی از «چه چیزی فعال است» داشته باشد
  • وقتی می‌خواهید در دسکتاپ پنل همیشه باز باشد و در موبایل به صورت Sheet روباز شود

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

  • فقط ۱-۲ فیلتر ساده دارید — از FilterBar (پایه) یا یک ردیف Button/Select افقی استفاده کنید
  • کاربر معمولاً فقط یک فیلتر را تغییر می‌دهد (مثل جستجوی ساده) — SearchInput یا Autocomplete تنها کافی است
  • بیش از ۱۰-۱۲ بخش دارید — پنل طولانی‌تر از ارتفاع viewport کاربر را گیج می‌کند؛ فیلترها را در چند صفحه یا tab بشکنید

فیلترها

طبقه‌بندی ۵‌گانه گرایش

زمین بازی

با تغییر تنظیمات زیر، پیش‌نمایش زنده را مشاهده کنید.

زمین بازی

فیلترها۲

تنظیمات
داده
3
حالت
2
محتوا
کد این نمونه به‌صورت خودکار قابل تولید نیست — برای کد آماده‌ی copy/paste به بخش «استفاده» در بالای صفحه مراجعه کنید.

استفاده

FilterPanel، state-agnostic است. شما state را با useState/useReducer/TanStack Query/… مدیریت می‌کنید و هر FilterSection یک slot برای ورودی مربوط به خودش فراهم می‌کند.

'use client'
import * as React from 'react'
import {
  FilterPanel,
  FilterPanelHeader,
  FilterPanelTitle,
  FilterPanelClearAll,
  FilterPanelBody,
  FilterPanelFooter,
  FilterSection,
  DateRangePicker,
  Checkbox,
  Button,
} from '@parto-system-design/ui'

const DEFAULT = { dateRange: undefined, platforms: [] as string[] }

export function AnalyticsFilters() {
  const [filters, setFilters] = React.useState(DEFAULT)
  const count = (filters.dateRange ? 1 : 0) + filters.platforms.length

  return (
    <aside className="w-80 border-s border-border">
      <FilterPanel>
        <FilterPanelHeader>
          <FilterPanelTitle activeCount={count} />
          {count > 0 && <FilterPanelClearAll onClear={() => setFilters(DEFAULT)} />}
        </FilterPanelHeader>

        <FilterPanelBody>
          <FilterSection
            title="بازه زمانی"
            activeCount={filters.dateRange ? 1 : 0}
            onClear={filters.dateRange ? () => setFilters((f) => ({ ...f, dateRange: undefined })) : undefined}
          >
            <DateRangePicker
              value={filters.dateRange}
              onChange={(range) => setFilters((f) => ({ ...f, dateRange: range }))}
              usePersianCalendar
            />
          </FilterSection>

          <FilterSection
            title="پلتفرم‌ها"
            activeCount={filters.platforms.length}
            onClear={filters.platforms.length ? () => setFilters((f) => ({ ...f, platforms: [] })) : undefined}
            defaultOpen={false}
          >
            {/* محتوای فیلتر */}
          </FilterSection>
        </FilterPanelBody>

        <FilterPanelFooter>
          <Button variant="outline" size="sm" onClick={() => setFilters(DEFAULT)}>
            پیش‌فرض
          </Button>
          <Button variant="primary" size="sm">
            اعمال
          </Button>
        </FilterPanelFooter>
      </FilterPanel>
    </aside>
  )
}

داخل Sheet (حالت Slide-in)

برای موبایل یا پنل قابل‌باز/بسته، FilterPanel را داخل Sheet قرار دهید. FilterPanelTrigger یک دکمه‌ی آماده با آیکون و badge شمارنده است.

<>
  <FilterPanelTrigger activeCount={count} onClick={() => setOpen(true)} />
  <Sheet open={open} onOpenChange={setOpen}>
    <SheetContent side="right" className="p-0 gap-0 flex flex-col">
      <SheetTitle className="sr-only">فیلترها</SheetTitle>
      <FilterPanel>{/* همان ساختار هدر/بدنه/فوتر */}</FilterPanel>
    </SheetContent>
  </Sheet>
</>

نکته‌ی a11y

Sheet روی Radix Dialog سوار است و برای screen reader باید یک SheetTitle داشته باشد. اگر عنوان را درون FilterPanelHeader نشان داده‌اید، یک <SheetTitle className="sr-only"> بگذارید تا تکرار بصری نداشته باشید.

ActiveFiltersBar

نوار چیپ فیلترهای فعال که بالای محتوای صفحه می‌نشیند — جدا از پنل، چون روی صفحه دائماً دیده می‌شود.

فیلترهای فعال:هفته گذشتهاینستاگرامخشممنتقد سازنده
<ActiveFiltersBar>
  {activeChips.map((chip) => (
    <FilterChip key={chip.id} label={chip.label} onRemove={chip.remove} />
  ))}
  {activeChips.length > 0 && <ActiveFiltersClearAll onClear={clearAll} />}
</ActiveFiltersBar>
  • label={false} برای حذف متن «فیلترهای فعال:» ابتدای نوار
  • hideWhenEmpty برای پنهان کردن کامل وقتی فیلتر فعالی نیست
  • چیپ‌ها را خودتان از state اصلی تولید می‌کنید — FilterChip.onRemove مستقیم state را برمی‌گرداند

FilterSection — انواع

این بخش به‌طور پیش‌فرض باز است. روی عنوان کلیک کنید تا جمع شود.

بخش غیرقابل‌جمع

برای فیلترهای تک‌فیلدی که همیشه قابل‌مشاهده باشند

  • پیش‌فرض — قابل‌جمع‌شدن با chevron، defaultOpen={true}
  • defaultOpen={false} — در رندر اول بسته است (مناسب بخش‌های پرکاربرد کمتر مثل «تنظیمات پیشرفته»)
  • activeCount — یک pill برند کنار عنوان ظاهر می‌شود؛ همراه با onClear یک دکمه‌ی × برای پاک کردن همان بخش
  • collapsible={false} — هدر static می‌شود (برای فیلدهای تک‌ورودی مثل جستجو که همیشه باید دیده شوند)
  • کنترل‌شدهopen + onOpenChange برای کنترل کامل از بیرون

Props

FilterPanel

Prop

Type

FilterPanelTitle

Prop

Type

FilterPanelClearAll

Prop

Type

FilterSection

Prop

Type

FilterPanelTrigger

Prop

Type

ActiveFiltersBar

Prop

Type

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

بکنید

  • state را بیرون از FilterPanel (در والد) نگه دارید و از همان‌جا activeCount و چیپ‌های ActiveFiltersBar را مشتق کنید - ActiveFiltersBar را بالای محتوای صفحه بگذارید و FilterPanel را کنار — کاربر همیشه باید بداند چه فیلترهایی فعال‌اند، حتی با پنل بسته - بخش‌های پرکاربرد را defaultOpen بگذارید و بخش‌های پیشرفته را defaultOpen={false} - اگر بخش فقط یک فیلد دارد و همیشه باید دیده شود، collapsible={false} بگذارید تا یک کلیک اضافی نباشد

نکنید

  • FilterPanel را به تنهایی برای ۱-۲ فیلتر استفاده نکنید — FilterBar افقی کافی است - بیش از ۸-۱۰ بخش در یک پنل نگذارید — یا tab کنید یا به چند صفحه بشکنید - activeCount را بدون FilterSection.onClear نگذارید — کاربر انتظار دارد دکمه‌ی پاک کردن ببیند - بعد از اعمال فیلتر، سایر state (URL, query cache) را فراموش نکنید به‌روز کنید — FilterPanel فقط UI است

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

  • FilterSection از @radix-ui/react-collapsible استفاده می‌کند — aria-expanded خودکار، Space/Enter برای toggle
  • ActiveFiltersBar دارای role="group" با aria-label="فیلترهای فعال" است
  • دکمه‌های × (clear-section, remove-chip) دارای aria-label اختصاصی مناسب
  • رنگ‌ها همگی از توکن‌های DS خوانده می‌شوند — در هر دو تم light/dark به درستی کنتراست دارند
  • انیمیشن expand/collapse با prefers-reduced-motion سازگار است

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

  • FilterBar — نوار فیلتر افقی ساده؛ وقتی پنل کامل نیاز نیست از این استفاده کنید
  • FilterChip — چیپ فیلتر با دکمه‌ی ×؛ داخل ActiveFiltersBar استفاده می‌شود
  • Sheet — برای حالت slide-in موبایل یا toggle
  • DateRangePicker — انتخاب بازه زمانی، معمول‌ترین ورودی FilterSection
  • MultiSelect — برای لیست پلتفرم‌ها یا برچسب‌ها به‌جای checkbox list