# Power Search

## Usage

Power Search splits a query into a field, an operator, and a value. Focusing the input lists the fields. Choosing one opens an editor when the operator still needs a value. A boolean field, an empty operator, and a content search become a filter immediately: the editor closes and focus returns to the search field. Clicking an existing empty-operator token does not open the built-in editor. It commits one edit. A consumer editor registered as `components.empty.editor`, or the editor slot, still opens.

Backspace on an empty input removes the last token. The arrow keys move between tokens, Enter opens the focused one, and Backspace or Delete removes it. A read-only token cannot be edited or removed. Escape cancels the editor and returns focus.

The editor follows the width of the search field, from about 400 to 720, and stays inside the layer gutter (16 on each side of the viewport). When the viewport cannot hold the minimum, the floor gives way to that gutter. The editor hugs the side that opened it only after the maximum has squeezed it. The position is taken once, while it is open. When the editor itself is narrower than 400, the fields stack. That follows the editor’s width, not the window’s.

`createPowerSearchConfig` builds the operators from field definitions and returns `applyFilters`. That function filters rows on its own. It does not go through a table’s column filters. Time, relative dates, entities, and custom values keep every row unless the caller interprets them. A nested filter requires every child by default. An operator named or, or one whose name contains any, requires only one child.

Tokens can collapse to one line when the field loses focus. The inline mode expands in place. The layer mode floats over the field and leaves the content below where it is. A numeric result count is read with plural wording. A string is shown as given. Changes are announced politely, and the first paint is silent.

Pass a disabled reason together with the disabled state. The input stays focusable, and the reason appears on hover and keyboard focus. Do not wrap the field in another tooltip. A natively disabled control swallows the hover.

```vue
<script setup lang="ts">
import { createPowerSearchConfig, PowerSearch } from '@astryx-vue/core/PowerSearch'
import { computed, ref } from 'vue'

const people = [
  { name: 'Alice', active: true },
  { name: 'Bob', active: false },
]
const { config, applyFilters } = createPowerSearchConfig([
  { key: 'name', type: 'string', label: 'Name' },
  { key: 'active', type: 'boolean', label: 'Active' },
])
const filters = ref([])
const matched = computed(() => applyFilters(filters.value, people))
</script>

<template>
  <PowerSearch v-model:filters="filters" :config="config" :result-count="matched.length" />
</template>
```

## Best practices

| Guidance | Practices |
| --- | --- |
| Do | Group a long field list, and add aliases for the fields people actually type. |
| Do | Point content search at one string field instead of letting every field consume free text. |
| Don’t | Fold Power Search into a table column filter. Column filters stay substring and enum matches. |
| Don’t | Save a nested filter while a child is unfinished. An unfinished child is not a filter yet. |

## Examples

### Fields and operators

Filter by name, role, active state, and age. The list below follows the current filters.

```vue
<script setup lang="ts">
import type { PowerSearchFilter } from '@astryx-vue/core/PowerSearch'
import { createPowerSearchConfig, PowerSearch } from '@astryx-vue/core/PowerSearch'
import { computed, ref } from 'vue'

interface Person {
  name: string
  role: string
  active: boolean
  age: number
}

const people: Person[] = [
  { name: 'Alice', role: 'engineer', active: true, age: 32 },
  { name: 'Bob', role: 'designer', active: false, age: 28 },
  { name: 'Carol', role: 'engineer', active: true, age: 41 },
  { name: 'Diego', role: 'manager', active: true, age: 36 },
]

const { config, applyFilters } = createPowerSearchConfig([
  { key: 'name', type: 'string', label: 'Name' },
  {
    key: 'role',
    type: 'enum',
    label: 'Role',
    enumValues: [
      { value: 'engineer', label: 'Engineer' },
      { value: 'designer', label: 'Designer' },
      { value: 'manager', label: 'Manager' },
    ],
  },
  { key: 'active', type: 'boolean', label: 'Active' },
  { key: 'age', type: 'number', label: 'Age' },
] as const, 'people')

const searchConfig = { ...config, contentSearchFieldKey: 'name' }
const filters = ref<PowerSearchFilter[]>([])
const matched = computed(() => applyFilters(filters.value, people))
</script>

<template>
  <div class="power-search-demo">
    <PowerSearch
      v-model:filters="filters"
      :config="searchConfig"
      :result-count="matched.length"
    />
    <ul>
      <li v-for="person in matched" :key="person.name">
        {{ person.name }}
      </li>
    </ul>
  </div>
</template>

<style scoped>
.power-search-demo {
  display: flex;
  flex-direction: column;
  gap: 12px;
  width: 100%;
}

ul {
  margin: 0;
  padding-left: 1.25rem;
}
</style>
```

### Overflow

On blur the tokens collapse to one line and the rest are counted. Focus expands them again.

```vue
<script setup lang="ts">
import type { PowerSearchFilter } from '@astryx-vue/core/PowerSearch'
import { createPowerSearchConfig, PowerSearch } from '@astryx-vue/core/PowerSearch'
import { ref } from 'vue'

const { config } = createPowerSearchConfig([
  { key: 'name', type: 'string', label: 'Name' },
  { key: 'city', type: 'string', label: 'City' },
  { key: 'team', type: 'string', label: 'Team' },
  { key: 'active', type: 'boolean', label: 'Active' },
] as const)

const filters = ref<PowerSearchFilter[]>([
  { field: 'name', operator: 'contains', value: { type: 'string', value: 'li' } },
  { field: 'city', operator: 'starts_with', value: { type: 'string', value: 'San' } },
  { field: 'team', operator: 'is', value: { type: 'string', value: 'Design' } },
  { field: 'active', operator: 'is_true', value: { type: 'empty' } },
])
</script>

<template>
  <div class="power-search-narrow">
    <PowerSearch
      v-model:filters="filters"
      :config="config"
      overflow="unfocused-inline"
    />
  </div>
</template>

<style scoped>
.power-search-narrow {
  width: min(100%, 280px);
}
</style>
```

### Nested filters

A match can require every child filter, or any one of them.

```vue
<script setup lang="ts">
import type { PowerSearchConfig, PowerSearchFilter } from '@astryx-vue/core/PowerSearch'
import { PowerSearch } from '@astryx-vue/core/PowerSearch'
import { ref } from 'vue'

const config: PowerSearchConfig = {
  name: 'people',
  fields: [
    {
      key: 'match',
      label: 'Match',
      defaultOperator: 'all',
      operators: [
        { key: 'all', label: 'all of', value: { type: 'nested' } },
        { key: 'any', label: 'any of', value: { type: 'nested' } },
      ],
    },
    {
      key: 'name',
      label: 'Name',
      defaultOperator: 'contains',
      operators: [
        { key: 'contains', i18nKey: '@astryx.powersearch.operator.contains', value: { type: 'string' } },
        { key: 'is', i18nKey: '@astryx.powersearch.operator.is', value: { type: 'string' } },
      ],
    },
    {
      key: 'active',
      label: 'Active',
      defaultOperator: 'is_true',
      operators: [
        { key: 'is_true', i18nKey: '@astryx.powersearch.operator.isTrue', value: { type: 'empty' } },
        { key: 'is_false', i18nKey: '@astryx.powersearch.operator.isFalse', value: { type: 'empty' } },
      ],
    },
  ],
}

const filters = ref<PowerSearchFilter[]>([])
</script>

<template>
  <PowerSearch v-model:filters="filters" :config="config" />
</template>
```

### Paused search

The filters stay put while the index rebuilds. The input can still take focus and explains why.

```vue
<script setup lang="ts">
import type { PowerSearchFilter } from '@astryx-vue/core/PowerSearch'
import { createPowerSearchConfig, PowerSearch } from '@astryx-vue/core/PowerSearch'
import { ref } from 'vue'

const { config } = createPowerSearchConfig([
  { key: 'name', type: 'string', label: 'Name' },
] as const)

const filters = ref<PowerSearchFilter[]>([
  { field: 'name', operator: 'contains', value: { type: 'string', value: 'Ada' }, isReadOnly: true },
])
</script>

<template>
  <PowerSearch
    v-model:filters="filters"
    :config="config"
    disabled
    disabled-reason="Search is paused while the index rebuilds."
  />
</template>
```

## Props

| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `config` | `PowerSearchConfig` | required | Fields, operators, and the value each operator expects. |
| `filters` | `PowerSearchFilter[]` | - | Current filters. When passed, the parent owns them and listens for updates. |
| `label` | `string` | Search | Accessible name of the input. |
| `isLabelHidden` | `boolean` | `true` | Hides the label visually and keeps it available to assistive tech. Omitted means hidden; `false` shows it. `labelHidden` is the older name. When both are set, this one wins. |
| `placeholder` | `string` | Search… | Placeholder while there are no filters. |
| `autofocus` | `boolean` | `false` | Focus the input after mount. |
| `hasClear` | `boolean` | `true` | Shows clear all once a filter exists. The older `clearable` name works too. |
| `readOnly` | `boolean` | `false` | Blocks adding, editing, and removing. |
| `disabled` | `boolean` | `false` | Turns the whole search off. |
| `disabledMessage` | `string` | - | Why it is off. With `disabled`, the message shows in a tooltip on hover or keyboard focus and the input stays focusable with `aria-disabled`. The older `disabledReason` name works too. |
| `status` | `'default' \| 'success' \| 'warning' \| 'error'` | `default` | Validation status. |
| `statusMessage` | `string` | - | Status text. |
| `statusVariant` | `'attached' \| 'detached'` | `attached` | Places the message against the field, or with a gap. |
| `maxTokenLength` | `number` | `40` | How many characters of a value a token shows. |
| `maxSearchResults` | `number` | `10` | Ranked field results for a non-empty query. An empty query browses up to 1,000 fields. |
| `maxOperatorMenuItems` | `number` | `10` | Suggestions in string and entity value editors. It does not cap the field menu or enum menus. |
| `menuWidth` | `number` | - | Exact pixel width of the field menu. It does not size the value editor. |
| `popoverSaveButtonLabel` | `string` | Apply | Label of the save button in the filter editor popover. The older `saveLabel` name works too. |
| `timezoneID` | `string` | - | Time zone used when a date is shown, such as America/New_York. The older `timeZone` name works too. |
| `overflow` | `'none' \| 'unfocused-inline' \| 'unfocused-layer'` | `none` | Wrap tokens, collapse them on blur, or expand them in a layer. |
| `resultCount` | `number \| string` | - | Match count. A number is formatted with the result plural. A string is shown as given. |
| `size` | `'sm' \| 'md' \| 'lg'` | `md` | Size of the input and the tokens. |
| `components` | `PowerSearchComponents` | - | Replace the token or the editor for one value type. |
| `containerClass` | `string` | - | Added to the field shell, the layer that holds the label and the status message. It does not land on the bordered box or the native input. |
| `containerStyle` | `StyleValue` | - | Inline style for the field shell. Same landing as `containerClass`. |
| `controlClass` | `string` | - | Added to the bordered box. A `class` on the component lands there too, not on the native input. |
| `controlStyle` | `StyleValue` | - | Inline style for the bordered box. A `style` on the component lands there too. |

## Events

| Name | Payload | Description |
| --- | --- | --- |
| `update:filters` | the next filters | Filters changed. Use it with the two-way binding. |
| `change` | filters, change type, index | The type is add, edit, or remove. The index is the filter that changed. |
| `focus` | focus event | Focus entered the input. |
| `blur` | focus event | Focus left the input. |

## Slots

The icon sits before the tokens. <code>endContent</code> (also accepted as <code>end</code>) sits before the clear button and is the place for another action. The token and editor slots win over a component registered for a value type.

The component also exposes focus and blur methods, so a button outside the field can send the cursor back.

## Field definitions

`createPowerSearchConfig` accepts string, number, boolean, date, enum, enum list, and string list. Strings can contain, start with, end with, or equal a value. Numbers compare magnitude. Dates can be before, after, or between. A boolean does not ask for another value.

A hand-written config can also use time, a relative date, an entity list, a custom editor, and nested filters. A custom editor needs a way to read the value back as text.
