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>
  );
}