# Filter Bar (`filter-bar`) - SignalOS widget

> Composable toolbar: debounce-ready search input, single/multi-select dropdown filters with removable badges, clear-all, and an action slot.

- **Version:** 0.2.0
- **Kind:** widget · **Category:** inputs
- **Install:** `npx shadcn@latest add @signalos/filter-bar`
- **Registry dependencies (pulled automatically):** @signalos/tokens, @signalos/utils, @signalos/badge, @signalos/button, @signalos/checkbox, @signalos/input, @signalos/popover, @signalos/select
- **npm dependencies:** lucide-react@^1.7.0
- **Files installed:** `src/components/widgets/filter-bar/FilterBar.tsx`, `src/components/widgets/filter-bar/DropdownFilter.tsx`, `src/components/widgets/filter-bar/FilterBar.types.ts`

## Access

This is a private registry: pulling source requires a `SIGNALOS_REGISTRY_TOKEN`
(GitHub fine-grained PAT with read access to the signal-widgets repo) and an
`@signalos` entry in components.json `"registries"`. Previews and this document are public.

## Usage

```tsx
import { FilterBar } from "@/components/widgets/filter-bar/FilterBar"
```

## Example

```tsx
// Example: controlled filter bar - search, single + multi select, clear-all.
import { useState } from "react"
import { Flag, User } from "lucide-react"

import { FilterBar } from "@/components/widgets/filter-bar/FilterBar"
import type { DropdownOption } from "@/components/widgets/filter-bar/FilterBar.types"

const severityOptions: DropdownOption[] = [
  { key: "critical", value: "Critical" },
  { key: "high", value: "High" },
  { key: "medium", value: "Medium" },
]

const ownerOptions: DropdownOption[] = [
  { key: "chen", value: "M. Chen" },
  { key: "okafor", value: "A. Okafor" },
]

export default function Example() {
  const [query, setQuery] = useState("")
  const [severity, setSeverity] = useState<string>()
  const [owners, setOwners] = useState<string[]>([])

  // Feed these three values into your list query (thunk, hook, etc.).
  return (
    <FilterBar
      search={{
        value: query,
        onChange: setQuery,
        placeholder: "Search signals...",
      }}
      filters={[
        {
          id: "severity",
          options: severityOptions,
          placeholder: "Severity",
          icon: <Flag className="h-4 w-4 text-muted-foreground" />,
          value: severity,
          onChange: (option) => setSeverity(option.key),
        },
        {
          id: "owner",
          multiSelect: true,
          options: ownerOptions,
          placeholder: "Owner",
          icon: <User className="h-4 w-4 text-muted-foreground" />,
          value: owners,
          onChange: (options) => setOwners(options.map((o) => o.key)),
        },
      ]}
      onClearAll={() => {
        setQuery("")
        setSeverity(undefined)
        setOwners([])
      }}
    />
  )
}
```

## Props

### `DropdownOption`

| Prop | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `key` | `string` | yes | - |  |
| `value` | `string` | yes | - |  |
| `icon` | `ReactNode` | no | - |  |

### `DropdownFilterProps`

```ts
export type DropdownFilterProps = {
  options: DropdownOption[]
  triggerIcon?: ReactNode
  /** @default "Select..." */
  placeholder?: string
  className?: string
  style?: CSSProperties
} & (
  | {
      multiSelect?: false
      selectedKey?: string
      onValueChange: (option: DropdownOption) => void
    }
  | {
      multiSelect: true
      selectedKey?: string[]
      onValueChange: (options: DropdownOption[]) => void
    }
)
```

### `FilterConfig`

```ts
export type FilterConfig = {
  /** Stable id, also used as the filter's data-testid suffix. */
  id: string
  options: DropdownOption[]
  placeholder?: string
  icon?: ReactNode
  className?: string
} & (
  | {
      multiSelect?: false
      value?: string
      onChange: (option: DropdownOption) => void
    }
  | {
      multiSelect: true
      value?: string[]
      onChange: (options: DropdownOption[]) => void
    }
)
```

### `FilterBarSearchConfig`

| Prop | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `value` | `string` | yes | - |  |
| `onChange` | `(value: string) => void` | yes | - |  |
| `placeholder` | `string \| undefined` | no | `"Search..."` |  |

### `FilterBarProps`

| Prop | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `search` | `FilterBarSearchConfig \| undefined` | no | - | Controlled search input; omit to hide the search box. |
| `filters` | `FilterConfig[] \| undefined` | no | - | Dropdown filters, single- or multi-select. |
| `onClearAll` | `(() => void) \| undefined` | no | - | Shows a Clear-all button (when anything is active) and receives the click. |
| `children` | `ReactNode` | no | - | Extra actions rendered at the right edge (buttons, toggles). |
| `className` | `string \| undefined` | no | - |  |
| `data-testid` | `string \| undefined` | no | `"filter-bar"` | Test identifier rendered as `data-testid` on the root element. |

## Changelog

# filter-bar

## 0.2.0

- Add a `"data-testid"` prop to `FilterBar` (`@default "filter-bar"`); the
  search input's id now derives from it (`${dataTestId}-search`,
  `${dataTestId}-clear`). Default behavior is unchanged.
- Fix `DropdownFilter`'s trigger stripping the focus ring
  (`focus:ring-0`) with no replacement; keyboard users now get a visible
  `focus-visible:ring-2` indicator on both the single- and multi-select
  trigger.
- The multi-select "remove" chip is now keyboard-operable (`tabIndex={0}` +
  Enter/Space), not just clickable.
- Swap raw brand-tint utilities (`bg-brand-muted-blue/5`,
  `bg-brand-teal-subtle`) used for ordinary hover/selected states to the
  semantic `bg-accent` / `text-accent-foreground` tokens, so a client theme
  override reaches them. Values are unchanged today since `--bg-accent` maps
  to the same brand-teal color; only the indirection changes.

## 0.1.1

- Content sync fix to bring source in line with published registry payload.


## 0.1.0

- `DropdownFilter` lifted near-verbatim from `signal-core-ui` `common/dropdown-filter.tsx`
  (single/multi discriminated union preserved). Non-token badge classes
  (`bg-primary-50` etc.) normalized to `brand-teal-subtle` / `text-accent` tokens.
- New `FilterBar` composition: search input, N dropdown filters, conditional
  clear-all, and a right-aligned action slot.
