# SearchPalette

Search results, actions and a detail view in one accessible modal.

- Group: Website
- Import: `import { SearchPalette } from '@misoto22/folio/website'`
- Page: https://ui.misoto22.com/components/search-palette/

## When to reach for it

Pass already-filtered results; the package owns the combobox and focus behavior.

## Best practices

### Do

- Supply localized labels, descriptive links and meaningful image alternatives.

### Don’t

- Do not put service credentials, routing logic or backend models inside presentation components.

## SearchPalette

Search, actions and an optional result detail inside one accessible modal.

### Props

- `open` (required) — `boolean`.
- `onOpenChange` (required) — `(open: boolean) => void`.
- `label` (required) — `string`.
- `inputLabel` (required) — `string`.
- `placeholder` (required) — `string`.
- `query` (required) — `string`.
- `onQueryChange` (required) — `(query: string) => void`.
- `groups` (required) — `SearchPaletteGroup[]`. Already filtered results; search and data access belong to the consumer.
- `emptyLabel` (required) — `ReactNode`.
- `labels` (required) — `{ close: string; navigate: string; select: string; back: string }`.
- `detail` — `{ label: string; content: ReactNode; onBack: () => void; onSubmit: () => void; submitDisabled?: boolean }`. Optional answer or detail view, with Escape returning to the results.
- `onContentLinkClick` — `() => void`.
- `onCloseAutoFocus` — `DialogContentProps['onCloseAutoFocus']`.

## SearchExcerpt

Render query highlights as text, never as consumer-supplied HTML.

### Props

- `segments` (required) — `ReadonlyArray<{ text: string; highlighted: boolean }>`.

## Example — host filtered results

```tsx
import { Button, Text } from '@misoto22/folio'
import { SearchPalette } from '@misoto22/folio/website'

<div className="min-h-[26rem]">
  <Button onClick={() => setOpen(true)}>Search the library</Button>
  <Text role="status" size="sm">{selected}</Text>
  <SearchPalette open={open} onOpenChange={setOpen} label="Library search" inputLabel="Search records"
    placeholder="Search titles" query={query} onQueryChange={setQuery} emptyLabel="No matching records"
    labels={{ close: 'Close search', navigate: 'Navigate', select: 'Select', back: 'Back to results' }}
    groups={[{ id: 'records', label: 'Records', items: titles.filter((title) => title.toLowerCase().includes(query.toLowerCase())).map((title) => ({
      id: title, title, onSelect: () => { setSelected(title); setOpen(false) },
    })) }]} />
</div>
```
