"use client"
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const quickFiltersVariants = cva("flex flex-wrap items-center", {
variants: {
size: {
default: "gap-1.5",
sm: "gap-1",
},
},
defaultVariants: { size: "default" },
})
// The chosen pill wears the kit's selected fill, --brand-muted, with its label
// in ink — the plane a selected row, chip or toggle takes — not the solid
// accent, which is the default Button's fill. An unchosen pill keeps its label
// in the quieter tier and the pointer gives it half the muted plane, so hover
// never paints what selected paints. The weight is 500 on every pill, so
// choosing one never reflows the row.
const quickFilterVariants = cva(
"inline-flex shrink-0 items-center rounded-md font-medium whitespace-nowrap transition-colors select-none focus-ring",
{
variants: {
size: {
default: "h-8 gap-1.5 px-2.5 text-sm",
sm: "h-7 gap-1 px-2 text-xs",
},
selected: {
true: "bg-brand-muted text-foreground",
false: "text-muted-foreground hover:bg-muted/50",
},
},
defaultVariants: { size: "default", selected: false },
}
)
export type QuickFilterOption = {
value: string
label: React.ReactNode
/** How many rows the filter would leave; shown in a quiet badge after the label. */
count?: number
}
export type QuickFiltersProps = React.ComponentProps<"div"> & {
options: QuickFilterOption[]
value: string
onValueChange: (value: string) => void
size?: NonNullable<VariantProps<typeof quickFiltersVariants>["size"]>
"aria-label"?: string
}
/** A row of one-tap filter pills — the two or three views a table is usually looked at through. */
function QuickFilters({
className,
options,
value,
onValueChange,
size = "default",
"aria-label": ariaLabel = "Quick filters",
...props
}: QuickFiltersProps) {
const tabRefs = React.useRef(new Map<string, HTMLButtonElement | null>())
// The first pill stands in for the tab stop when `value` matches nothing, so
// the row is always reachable by keyboard.
const tabStop = options.some((option) => option.value === value) ? value : options[0]?.value
function move(from: number, direction: 1 | -1) {
const count = options.length
if (count === 0) return
const next = options[(((from + direction) % count) + count) % count]
onValueChange(next.value)
tabRefs.current.get(next.value)?.focus()
}
function handleKeyDown(event: React.KeyboardEvent<HTMLButtonElement>, index: number) {
if (event.key === "ArrowRight" || event.key === "ArrowDown") {
event.preventDefault()
move(index, 1)
} else if (event.key === "ArrowLeft" || event.key === "ArrowUp") {
event.preventDefault()
move(index, -1)
}
}
return (
<div
data-slot="quick-filters"
data-size={size}
role="tablist"
aria-label={ariaLabel}
className={cn(quickFiltersVariants({ size }), className)}
{...props}
>
{options.map((option, index) => {
const selected = option.value === value
return (
<button
key={option.value}
ref={(node) => {
tabRefs.current.set(option.value, node)
}}
type="button"
role="tab"
aria-selected={selected}
data-slot="quick-filter"
data-selected={selected || undefined}
tabIndex={option.value === tabStop ? 0 : -1}
onClick={() => onValueChange(option.value)}
onKeyDown={(event) => handleKeyDown(event, index)}
className={quickFilterVariants({ size, selected })}
>
{option.label}
{option.count === undefined ? null : (
<span
data-slot="quick-filter-count"
className={cn(
"rounded px-1 text-xs tabular-nums",
selected ? "bg-background text-foreground" : "bg-muted text-muted-foreground"
)}
>
{option.count}
</span>
)}
</button>
)
})}
</div>
)
}
export { QuickFilters, quickFilterVariants, quickFiltersVariants }