dodil/erp-ui
src/components/filter-bar/FilterBar.tsx
/**
* FilterBar — search and select filters above a list. Controlled: the screen owns the values (so they can
* live in the URL and survive a reload), the bar only shows and changes them.
*/
import { Button, Flex, Select, TextField } from "@radix-ui/themes";
import type { ReactNode } from "react";
export interface FilterOption { value: string; label: string }
export interface SelectFilter {
key: string;
label: string;
options: FilterOption[];
value: string | null;
onChange: (value: string | null) => void;
}
export interface FilterBarProps {
search?: { value: string; onChange: (v: string) => void; placeholder?: string };
filters?: SelectFilter[];
/** Buttons on the right, e.g. "New partner". */
actions?: ReactNode;
}
const ALL = "__all__";
export function FilterBar({ search, filters = [], actions }: FilterBarProps) {
const active = (search?.value ?? "") !== "" || filters.some((f) => f.value !== null);
return (
<Flex gap="2" align="center" wrap="wrap" role="search">
{search && (
<TextField.Root
aria-label={search.placeholder ?? "Search"}
placeholder={search.placeholder ?? "Search"}
value={search.value}
onChange={(e) => search.onChange(e.target.value)}
style={{ minWidth: 220 }}
/>
)}
{filters.map((f) => (
<Select.Root key={f.key} value={f.value ?? ALL} onValueChange={(v) => f.onChange(v === ALL ? null : v)}>
<Select.Trigger aria-label={f.label} placeholder={f.label} />
<Select.Content>
<Select.Item value={ALL}>All {f.label.toLowerCase()}</Select.Item>
{f.options.map((o) => (
<Select.Item key={o.value} value={o.value}>{o.label}</Select.Item>
))}
</Select.Content>
</Select.Root>
))}
{active && (
<Button variant="ghost" color="gray" onClick={() => { search?.onChange(""); filters.forEach((f) => f.onChange(null)); }}>
Clear
</Button>
)}
{actions && <Flex ml="auto" gap="2">{actions}</Flex>}
</Flex>
);
}