# Filter bar

Build filters one at a time: pick a field, a condition and a value (text, a number, one of the field's options, or a time window with presets like "Last 7 days"), then Add. The rules show as chips, each removable, with Clear all. The bar gives you `FilterRule[]` to query with; `useFilterChips` gives the same chips anywhere else.

Nothing is added until the person presses Add, and Add says what is missing until the rule is whole. The bar never filters anything itself: it hands the rules to you.

- Registry name: `filter-bar` (`@candor/filter-bar`)
- Tier: Pro. Installing it needs a Pro, Premium or Team licence key: the person runs `npx candor-ui key <key>` themselves; never ask for the key in chat or write it to a file.
- Docs: https://candor.themethodic.com/docs/filter-bar

## Install

```bash
npx candor-ui add filter-bar
```

Run `npx candor-ui@latest init` once first (https://candor.themethodic.com/get-started.md). Without the CLI: `npx shadcn@latest add @candor/filter-bar`.
It brings in: button, data, date-range-picker, filter-chips, input, label, locale, locked, number-input, popover, select, tokens, utils.
npm packages: lucide-react@^1.67.0.

## Example

```tsx
"use client"

import * as React from "react"
import { FilterBar, type FilterField, type FilterRule } from "@/components/filter-bar"

const fields: FilterField[] = [
  {
    id: "status",
    label: "Status",
    kind: "option",
    options: [
      { value: "running", label: "Running" },
      { value: "waiting", label: "Waiting on a person" },
      { value: "failed", label: "Failed" },
    ],
  },
  { id: "agent", label: "Agent", kind: "text" },
  { id: "cost", label: "Cost (USD)", kind: "number" },
  { id: "started", label: "Started", kind: "date" },
]

// The bar hands you the rules; your query applies them. Here they're only printed.
export default function Example() {
  const [rules, setRules] = React.useState<FilterRule[]>([{ id: "seed", field: "status", operator: "isNot", value: "failed" }])
  return (
    <div className="flex w-full flex-col gap-3">
      <FilterBar fields={fields} value={rules} onValueChange={setRules} />
      <pre className="overflow-x-auto rounded-md bg-muted p-3 text-xs">{JSON.stringify(rules.map(({ field, operator, value }) => ({ field, operator, value })), null, 2)}</pre>
    </div>
  )
}
```

## <FilterBar>

Props:

- `fields: FilterField[]`: What the person can filter on.
- `value?: FilterRule[] | undefined`: The rules, when you control them.
- `defaultValue?: FilterRule[] | undefined`: The rules at first, when you don't control them.
- `onValueChange?: ((rules: FilterRule[]) => void) | undefined`: Called with every rule each time one is added or removed.
- `className?: string | undefined`: Classes for the bar.

## Functions

### useFilterChips

```ts
useFilterChips(rules: FilterRule[], fields: FilterField[])
```

The chips for a set of rules, in the locale's words: "Status: not Closed", "Cost: more than 5". Pass them to `FilterChips` wherever the active filters show. A rule whose field is gone shows its field id and raw value.

## Values

### filterOperators

The conditions each kind offers, the first being the one picked when the field is.

```ts
const filterOperators: Record<FilterFieldKind, FilterOperator[]>
```

## Strings (`filterBar`)

The fixed words, English by default. Override any of them under `filterBar` in `<LocaleProvider strings={{ filterBar: { … } }}>`; the table is exported as `filterBarStrings`.

- `add`: Add filter
- `field`: Field
- `operator`: Condition
- `value`: Value
- `chooseField`: Choose a field
- `chooseValue`: Choose a value
- `apply`: Add
- `needField`: Choose a field first.
- `needValue`: Fill in a value first.
- `is`: is
- `isNot`: is not
- `contains`: contains
- `doesNotContain`: doesn’t contain
- `equals`: equals
- `notEquals`: doesn’t equal
- `greaterThan`: is more than
- `lessThan`: is less than
- `within`: is within
- `chipIs`: {value}
- `chipIsNot`: not {value}
- `chipContains`: contains “{value}”
- `chipDoesNotContain`: doesn’t contain “{value}”
- `chipEquals`: {value}
- `chipNotEquals`: not {value}
- `chipGreaterThan`: more than {value}
- `chipLessThan`: less than {value}
- `chipWithin`: {value}

## Types

### FilterFieldKind

What a field holds, which decides its conditions and how its value is entered.

```ts
"text" | "number" | "option" | "date"
```

### FilterOperator

How a rule compares the field with its value. Which ones a field offers follows its kind.

```ts
"is" | "isNot" | "contains" | "doesNotContain" | "equals" | "notEquals" | "greaterThan" | "lessThan" | "within"
```

### FilterField

Something a person can filter on.

```ts
{
  /** Kept on each rule as `field`. */
  id: string;
  /** The field's name in your app's words: "Status", "Owner", "Started". */
  label: string;
  /** What it holds: free text, a number, one of `options`, or a time. */
  kind: FilterFieldKind;
  /** For an `option` field: the values to choose from. */
  options?: {
    value: string;
    label: string;
  }[] | undefined;
  /** The conditions to offer, when not all of the kind's. */
  operators?: FilterOperator[] | undefined;
  /** For a `date` field: the windows to offer. Defaults to DateRangePicker's. */
  presets?: DateRangePreset[] | undefined;
}
```

### FilterRule

One filter the person added: a field, a condition and a value. Your query reads these.

```ts
{
  /** The rule's own id, which is also its chip's. */
  id: string;
  /** The `id` of the field it filters. */
  field: string;
  /** How it compares. */
  operator: FilterOperator;
  /** Text for a `text` field, an option's `value` for an `option` field, a number, or a time window. */
  value: string | number | DateRangeValue;
}
```

## Rules for using Candor UI

- Components take plain typed data. Never pass model output as HTML or render it as a link; messages are typed blocks.
- A control that can't be used says why: wrap it in `<Locked reason="…">`, don't just disable it.
- Don't show a value you don't have: leave it out (it renders as —) rather than guess. An estimate is labelled as one.
- Nothing is pre-selected in a decision a person makes, and the agent is named from your app's data, never from model output.
- Your app's words go in `LocaleProvider` (fixed strings) and `TermsProvider` (nouns); entity kinds and agent roles in `EntityKindsProvider` and `AgentRolesProvider`.
