PivotFilter
Horizontal or vertical strip of count-tagged pills used to quickly slice a list by one dimension — status, user, date bucket, or any other categorical value.
Installation
pnpm dlx @docyrus/cli add @docyrus/ui-pivot-filternpx shadcn@latest add avatar button popover skeletonOverview
PivotFilter is the React port of the Vue KvPivotFilter used across Docyrus apps. It renders a row (or column) of pills where each pill represents a bucket — a status option, a user, a day of the week, an enum value — and shows a count (or any aggregated stat) inside the pill.
Use it as a quick one-dimension filter above a data table, calendar, or map. Tapping a pill emits a single onSelect(item); the "All" pill emits onSelect(null).
The component is purely presentational — it has no opinion about how the items / counts are computed. Pair it with useDocyrusPivotFilter when you want a Docyrus data-source–backed version.
Anatomy
┌───────────────────────────────────────────────────────────────────┐
│ ⟳ All 62 │ ● New 12 │ ● In Progress 8 │ ● Review 3 │ … scroll →│
│ ⚙ │
└───────────────────────────────────────────────────────────────────┘
│ │ │
│ │ └── stat (count / sum / etc.)
│ └─────── label
└─ optional toolbar (refresh + settings popover)| Slot | Purpose |
|---|---|
| Toolbar | Optional. Shows refresh + settings buttons. Hidden when neither onRefresh nor settingsContent is provided. |
| All pill | First item, always selected when selectedItemId === null. Hide with hideAllPill. |
| Item pills | One per items[] entry. Render icon / avatar / name / secondary text / stat. |
Usage
Status filter
'use client';
import { useState } from 'react';
import { PivotFilter, type PivotFilterItem } from '@docyrus/ui/components/pivot-filter';
const items: Array<PivotFilterItem> = [
{ id: 'new', name: 'New', stat: 12, color: 'sky', icon: 'fal circle' },
{ id: 'in_progress', name: 'In Progress', stat: 8, color: 'amber', icon: 'fal spinner' },
{ id: 'done', name: 'Done', stat: 27, color: 'emerald', icon: 'fal circle-check' },
{ id: 'empty', name: 'Not Set', stat: 4, isEmpty: true }
];
export function StatusPivotFilter() {
const [selected, setSelected] = useState<string | null>(null);
const total = items.reduce((sum, item) => sum + item.stat, 0);
return (
<PivotFilter
items={items}
selectedItemId={selected}
onSelect={(item) => setSelected(item ? item.id : null)}
total={total} />
);
}User filter (avatars)
When an item has a user payload, its pill renders an avatar instead of an icon. Pass picturePath to show a profile picture; otherwise the initials fallback is used.
const users: Array<PivotFilterItem> = [
{
id: 'u_1',
name: 'Eray Bulut',
stat: 14,
user: { id: 'u_1', name: 'Eray Bulut', picturePath: 'https://…/avatar.jpg' }
},
{
id: 'u_2',
name: 'Selin Yıldız',
stat: 9,
user: { id: 'u_2', name: 'Selin Yıldız', picturePath: null }
}
];Vertical layout
vertical flips the strip to a left-rail style column — useful as a side filter next to a grid.
<div className="h-72 w-56">
<PivotFilter items={items} selectedItemId={selected} onSelect={…} vertical />
</div>Compact layout
compact collapses the name and stat onto a single row inside each pill — preferred when horizontal space is tight.
With a settings popover
Pass a settingsContent node and a gear button appears in the toolbar. The popover is unstyled inside — drop in any controls you want (date filter type, calculation function, etc.).
<PivotFilter
items={items}
selectedItemId={selected}
onSelect={setSelected}
onRefresh={() => refetch()}
settingsContent={
<div className="space-y-3">
<RadioGroup value={dateFilterType} onValueChange={setDateFilterType}>
{/* … */}
</RadioGroup>
<Button onClick={saveCalculation}>Save</Button>
</div>
} />The popover open state is internal by default; pass settingsOpen + onSettingsOpenChange if you need to control it.
Loading state
Pass loading while items are being fetched. The component renders two skeleton pills while keeping the "All" pill mounted so the selection is not lost.
<PivotFilter items={[]} selectedItemId={null} onSelect={…} loading />Colors
Pills accent on selection. The accent color is derived from each item's color field through this pipeline:
- If
coloralready includes a Tailwind shade (e.g.'emerald-600','rose-300'), use it as-is. - Otherwise treat it as a family name and append
-500(e.g.'sky'→'sky-500'). - Resolve the resulting token to a hex value through
resolveColorHex(it also accepts hex / rgb / hsl / CSS variables as-is). - When
coloris omitted, the pill falls back tosky-500.
This means status / select / radio-group options that already carry a color value from Docyrus enum metadata work without any extra mapping.
Empty items
Items flagged isEmpty: true are treated as the "Not Set" bucket:
- they render with a muted (
opacity-60) style, - they are automatically hidden when their
statis0, regardless ofhideZeroValues, - they are still emitted via
onSelectwhen the user taps them.
Hiding zero-count items
Set hideZeroValues to hide every item whose stat === 0, except the currently selected one (so the user can always reset). Use this when buckets are dynamically derived (e.g. user list, date buckets) and you don't want empty pills crowding the strip.
API Reference
<PivotFilter>
| Prop | Type | Default | Description |
|---|---|---|---|
items | Array<PivotFilterItem> | — | Items rendered in the strip. |
selectedItemId | string | null | — | Currently selected item id, or null when the "All" pill is active. |
onSelect | (item: PivotFilterItem | null) => void | — | Fired when an item is selected. The "All" pill fires with null. |
total | number | — | Aggregate stat displayed on the "All" pill. |
totalLabel | string | translated "All" | Label for the "All" pill. |
loading | boolean | false | Renders two skeleton pills instead of items. |
vertical | boolean | false | Vertical column layout. |
compact | boolean | false | Collapse name + stat onto a single row inside each pill. |
hideZeroValues | boolean | false | Hide items whose stat === 0 (except the currently selected one). |
hideAllPill | boolean | false | Hide the "All" pill — use when a filter is required. |
onRefresh | () => void | — | Refresh button click handler. Pass undefined to hide the refresh button. |
settingsContent | ReactNode | — | Content rendered inside the settings popover. When undefined, the gear button is hidden. |
settingsOpen | boolean | uncontrolled | Controlled open state for the settings popover. |
onSettingsOpenChange | (open: boolean) => void | — | Called when the popover open state changes. |
emptyState | ReactNode | built-in | Custom placeholder when there are no items and the "All" pill is hidden. |
className | string | — | Wrapper class name. |
PivotFilterItem
| Field | Type | Description |
|---|---|---|
id | string | Stable identifier. Used to match selectedItemId. |
name | string | Display label rendered in the pill. |
stat | number | Numeric value shown on the right side of the pill (count, sum, etc.). |
secondary | string | Secondary text (e.g. "to 14" for hours, "01.01 - 07.01" for week ranges). |
color | string | Tailwind color token (e.g. 'sky', 'emerald-500') or hex. Drives selection accents. |
icon | string | Docyrus icon identifier (e.g. 'huge building-06', 'fal star'). Rendered via DocyrusIcon. |
user | PivotFilterUser | When present, an avatar is rendered instead of an icon. |
isEmpty | boolean | Marks the synthetic "Not Set" bucket — muted style, auto-hidden at stat === 0. |
meta | Record<string, unknown> | Arbitrary payload returned to onSelect. Not read by the component. |
PivotFilterUser
| Field | Type | Description |
|---|---|---|
id | string | User id. |
name | string | Display name. Initial is used as the avatar fallback. |
picturePath | string | null | Avatar URL. Falls back to initials when absent. |
Components
| Component | Description |
|---|---|
PivotFilter | Main strip — orchestrates toolbar, "All" pill, item list, loading state. |
PivotFilterPill | Internal pill renderer — exported for advanced custom layouts. |
Type Exports
| Type | Description |
|---|---|
PivotFilterItem | Item shape — id, name, stat, plus optional icon / user / color / secondary / isEmpty / meta. |
PivotFilterProps | Component prop shape. |
PivotFilterUser | { id, name, picturePath? } for the user-rendered variant. |
i18n keys
The component reads these keys via useUiTranslation(). English fallbacks are bundled, so no provider is required.
| Key | Default |
|---|---|
ui.pivotFilter.all | All |
ui.pivotFilter.refresh | Refresh |
ui.pivotFilter.settings | Settings |
ui.pivotFilter.empty | No data |
When to reach for the hook
If you want the same UI bound to a Docyrus data source — auto-running a pivot query with COUNT_OF_id (or any other aggregate), reacting to active filters, and emitting a query rule when the user picks a bucket — use useDocyrusPivotFilter instead. It produces the items / total / loading / onRefresh / settingsContent props for you.
Pivot Calendar
Calendar component that pivots time-based statistics across four layouts — full month, days of a week, days of a month, or months of a year — with grouping, tooltips, and local or remote aggregation.
Pivot Grid
A standalone pivot grid with 3-level row and column hierarchies, subtotals, grand totals, drilldown, pinning, resizing, and export.