Components

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.

Client Only

Installation

pnpm dlx @docyrus/cli add @docyrus/ui-pivot-filter
UI Primitives(4 components)
npx shadcn@latest add avatar button popover skeleton

Overview

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)
SlotPurpose
ToolbarOptional. Shows refresh + settings buttons. Hidden when neither onRefresh nor settingsContent is provided.
All pillFirst item, always selected when selectedItemId === null. Hide with hideAllPill.
Item pillsOne 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:

  1. If color already includes a Tailwind shade (e.g. 'emerald-600', 'rose-300'), use it as-is.
  2. Otherwise treat it as a family name and append -500 (e.g. 'sky' → 'sky-500').
  3. Resolve the resulting token to a hex value through resolveColorHex (it also accepts hex / rgb / hsl / CSS variables as-is).
  4. When color is omitted, the pill falls back to sky-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 stat is 0, regardless of hideZeroValues,
  • they are still emitted via onSelect when 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>

PropTypeDefaultDescription
itemsArray<PivotFilterItem>—Items rendered in the strip.
selectedItemIdstring | 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.
totalnumber—Aggregate stat displayed on the "All" pill.
totalLabelstringtranslated "All"Label for the "All" pill.
loadingbooleanfalseRenders two skeleton pills instead of items.
verticalbooleanfalseVertical column layout.
compactbooleanfalseCollapse name + stat onto a single row inside each pill.
hideZeroValuesbooleanfalseHide items whose stat === 0 (except the currently selected one).
hideAllPillbooleanfalseHide the "All" pill — use when a filter is required.
onRefresh() => void—Refresh button click handler. Pass undefined to hide the refresh button.
settingsContentReactNode—Content rendered inside the settings popover. When undefined, the gear button is hidden.
settingsOpenbooleanuncontrolledControlled open state for the settings popover.
onSettingsOpenChange(open: boolean) => void—Called when the popover open state changes.
emptyStateReactNodebuilt-inCustom placeholder when there are no items and the "All" pill is hidden.
classNamestring—Wrapper class name.

PivotFilterItem

FieldTypeDescription
idstringStable identifier. Used to match selectedItemId.
namestringDisplay label rendered in the pill.
statnumberNumeric value shown on the right side of the pill (count, sum, etc.).
secondarystringSecondary text (e.g. "to 14" for hours, "01.01 - 07.01" for week ranges).
colorstringTailwind color token (e.g. 'sky', 'emerald-500') or hex. Drives selection accents.
iconstringDocyrus icon identifier (e.g. 'huge building-06', 'fal star'). Rendered via DocyrusIcon.
userPivotFilterUserWhen present, an avatar is rendered instead of an icon.
isEmptybooleanMarks the synthetic "Not Set" bucket — muted style, auto-hidden at stat === 0.
metaRecord<string, unknown>Arbitrary payload returned to onSelect. Not read by the component.

PivotFilterUser

FieldTypeDescription
idstringUser id.
namestringDisplay name. Initial is used as the avatar fallback.
picturePathstring | nullAvatar URL. Falls back to initials when absent.

Components

ComponentDescription
PivotFilterMain strip — orchestrates toolbar, "All" pill, item list, loading state.
PivotFilterPillInternal pill renderer — exported for advanced custom layouts.

Type Exports

TypeDescription
PivotFilterItemItem shape — id, name, stat, plus optional icon / user / color / secondary / isEmpty / meta.
PivotFilterPropsComponent 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.

KeyDefault
ui.pivotFilter.allAll
ui.pivotFilter.refreshRefresh
ui.pivotFilter.settingsSettings
ui.pivotFilter.emptyNo 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.

On this page