# Command

> La búsqueda de iCloud: un search field y los resultados como filas de menú, con ícono y detalle.

```tsx
import { Command, CommandDialog, CommandEmpty, CommandFilter, … } from "sebs7n-ui/command"
```

## Ejemplos

### Buscar en facturación

`CommandDialog` abierto por un botón o por ⌘J: el campo de búsqueda de iCloud, los filtros y los resultados como filas de menú. Enter abre el elegido. En una app el atajo es ⌘K; acá ⌘K ya es el buscador del sitio, que también es un `CommandDialog`.

```tsx
import { Button } from "sebs7n-ui/button"
import { CommandDialog, CommandFilter, CommandFilters, CommandInput } from "sebs7n-ui/command"
import { Kbd } from "sebs7n-ui/kbd"
import { SearchIcon } from "lucide-react"
import { useEffect, useState } from "react"

function Basico() {
  const [abierto, setAbierto] = useState(false)
  const [filtro, setFiltro] = useState("todo")
  const [elegido, setElegido] = useState<string | null>(null)

  // El atajo lo registra la app: el componente no escucha teclas globales. ⌘J y no ⌘K porque en
  // este sitio ⌘K abre el buscador, y con los dos escuchando se abrirían dos paletas.
  useEffect(() => {
    const onKeyDown = (event: KeyboardEvent) => {
      if (event.key.toLowerCase() !== "j" || !(event.metaKey || event.ctrlKey)) return
      event.preventDefault()
      setAbierto((previo) => !previo)
    }
    window.addEventListener("keydown", onKeyDown)
    return () => window.removeEventListener("keydown", onKeyDown)
  }, [])

  return (
    <div className="flex flex-col items-center gap-3">
      <Button aria-keyshortcuts="Meta+J" onClick={() => setAbierto(true)} variant="secondary">
        <SearchIcon />
        Buscar en facturación
        <Kbd size="sm">⌘J</Kbd>
      </Button>
      {elegido && <p className="text-body text-label-secondary">Elegiste: {elegido}</p>}
      <CommandDialog labels={{ dialog: "Buscar en facturación" }} onOpenChange={setAbierto} open={abierto}>
        <CommandInput placeholder="Facturas, clientes, acciones…" />
        <CommandFilters onValueChange={setFiltro} value={filtro}>
          <CommandFilter value="todo">Todo</CommandFilter>
          <CommandFilter value="facturas">Facturas</CommandFilter>
          <CommandFilter value="clientes">Clientes</CommandFilter>
        </CommandFilters>
        <Resultados
          filtro={filtro}
          onSelect={(value) => {
            setElegido(value)
            setAbierto(false)
          }}
        />
      </CommandDialog>
    </div>
  )
}
```

### Incrustado

`Command` sin diálogo, adentro de una superficie que pone quien lo ubica. El campo trae su propio relleno, así que se ve igual sobre cualquier fondo.

```tsx
import { Command, CommandInput } from "sebs7n-ui/command"
import { useState } from "react"

function Incrustado() {
  const [elegido, setElegido] = useState<string | null>(null)
  return (
    <div className="flex w-full max-w-md flex-col gap-2">
      <Command className="rounded-surface bg-grouped p-1">
        <CommandInput />
        <Resultados filtro="todo" onSelect={setElegido} />
      </Command>
      {elegido && <p className="text-body text-label-secondary">Elegiste: {elegido}</p>}
    </div>
  )
}
```

## Props

### Command

Hereda las props de `<div>`.

| Prop | Tipo | Por defecto | Descripción |
|---|---|---|---|
| `defaultValue` | `string` | `""` | El valor inicial. Es la versión no controlada de `value`. |
| `labels` | `Partial<Labels["command"]>` | — | Textos de la interfaz, para traducir o ajustar el tono. |
| `onValueChange` | `(value: string) => void` | — | Se llama con el valor nuevo cada vez que cambia. |
| `shouldFilter` | `boolean` | `true` | Con `false` no filtra: muestra los `CommandItem` que reciba, en su orden. |
| `value` | `string` | — | Lo escrito en el campo. Pasarlo lo vuelve controlado. |
| `className` | `string` | — | Se fusiona con las clases del componente vía `cn()` (tailwind-merge): lo que pongas gana. |

### CommandDialog

Hereda las props de `Dialog.Root`.

| Prop | Tipo | Por defecto | Descripción |
|---|---|---|---|
| `defaultValue` | `string` | — | El valor inicial. Es la versión no controlada de `value`. |
| `labels` | `Partial<Labels["command"]>` | — | Textos de la interfaz, para traducir o ajustar el tono. |
| `onValueChange` | `(value: string) => void` | — | Se llama con el valor nuevo cada vez que cambia. |
| `shouldFilter` | `boolean` | — | Con `false` no filtra: muestra los `CommandItem` que reciba, en su orden. Es para cuando la app ya filtró y ordenó —un ranking propio, una búsqueda en el servidor—. |
| `value` | `string` | — | Lo escrito en el campo. Pasarlo lo vuelve controlado. |
| `className` | `string` | — | Clases del panel. |
| `defaultOpen` | `boolean` | — | **Heredada de Base UI.** Si arranca abierto. Es la versión no controlada de `open`. |
| `onOpenChange` | `((open: boolean, eventDetails: DialogRoot.ChangeEventDetails) => void)` | — | **Heredada de Base UI.** Se llama con el estado nuevo cada vez que se abre o se cierra. |
| `open` | `boolean` | — | **Heredada de Base UI.** Si está abierto. Pasarla lo vuelve controlado: sin `onOpenChange` ya no se cierra solo. |

### CommandEmpty

Hereda las props de `<div>`.

| Prop | Tipo | Por defecto | Descripción |
|---|---|---|---|
| `className` | `string` | — | Se fusiona con las clases del componente vía `cn()` (tailwind-merge): lo que pongas gana. |

### CommandFilter

Hereda las props de `Radio.Root`.

| Prop | Tipo | Por defecto | Descripción |
|---|---|---|---|
| `className` | `string` | — | Se fusiona con las clases del componente vía `cn()` (tailwind-merge): lo que pongas gana. |

### CommandFilters

Hereda las props de `RadioGroup`.

| Prop | Tipo | Por defecto | Descripción |
|---|---|---|---|
| `onValueChange` | `(value: string) => void` | — | El filtro marcado. Siempre hay uno: tocar el marcado no lo apaga. |
| `className` | `string` | — | Se fusiona con las clases del componente vía `cn()` (tailwind-merge): lo que pongas gana. |

### CommandGroup

Hereda las props de `Autocomplete.Group`.

| Prop | Tipo | Por defecto | Descripción |
|---|---|---|---|
| `heading` | `React.ReactNode` | — | El título de la sección, con el mismo estilo que los títulos de grupo de los menús. Un grupo sin resultados se esconde entero. |
| `className` | `string` | — | Se fusiona con las clases del componente vía `cn()` (tailwind-merge): lo que pongas gana. |

### CommandInput

Hereda las props de `Autocomplete.Input`.

| Prop | Tipo | Por defecto | Descripción |
|---|---|---|---|
| `placeholder` | `string` | — | Lo que dice el campo vacío. Por defecto, `labels.placeholder` («Buscar»), que también es su nombre para el lector. |
| `wrapperClassName` | `string` | — | Clases de la caja del campo: la que lleva el relleno, la lupa y el anillo de foco. |
| `className` | `string` | — | Se fusiona con las clases del componente vía `cn()` (tailwind-merge): lo que pongas gana. |

### CommandItem

Hereda las props de `Autocomplete.Item`.

| Prop | Tipo | Por defecto | Descripción |
|---|---|---|---|
| `value` * | `string` | — | Identifica el ítem y es lo que recibe `onSelect`. Puede repetirse (el mismo cliente en dos grupos): esconder uno al filtrar no afecta al otro. |
| `description` | `React.ReactNode` | — | El detalle en gris, en un segundo renglón de 12 px. |
| `icon` | `React.ReactNode` | — | En una caja de 30 px, en el acento, como los íconos de los menús de iCloud. Un ícono de lucide va a 16 px; una imagen, a 20. |
| `keywords` | `readonly string[]` | — | Palabras que también lo encuentran, además del título. |
| `onClick` | `BaseUIComponentProps<'div', AutocompleteItemState>['onClick']` | — | Corre antes que `onSelect`, con el evento. Para lo que solo necesita el valor, `onSelect`. |
| `onSelect` | `(value: string) => void` | — | Enter o click. Recibe el `value`. |
| `textValue` | `string` | — | El título en texto plano, si `children` no es un string. Es lo que se filtra. |
| `className` | `string` | — | Se fusiona con las clases del componente vía `cn()` (tailwind-merge): lo que pongas gana. |

`*` obligatoria.

### CommandList

Hereda las props de `Autocomplete.List`.

| Prop | Tipo | Por defecto | Descripción |
|---|---|---|---|
| `className` | `string` | — | Se fusiona con las clases del componente vía `cn()` (tailwind-merge): lo que pongas gana. |

## Teclado

| Tecla | Qué hace |
|---|---|
| Escribir | Filtra por título y `keywords`, sin distinguir mayúsculas ni tildes. |
| ↑ ↓ | Mueven el elegido. |
| Enter | Ejecuta el `onSelect` del elegido. |
| Tab | Sale del campo, como en cualquier combobox: no completa nada. |
| Escape | Cierra `CommandDialog`. |

## Accesibilidad

- El campo es un `combobox` y la lista un `listbox` de Base UI (Autocomplete en modo `inline`): el lector anuncia el elegido mientras el foco se queda en el campo.
- `CommandFilters` es un `radiogroup` nombrado con `labels.filters` («Filtros»): siempre hay un filtro marcado, las flechas los recorren y Tab sale del grupo. El marcado, en el acento, lleva el anillo de foco inverso.
- `CommandEmpty` es una región `status` que queda siempre montada: si apareciera recién con el texto, varios lectores no lo anunciarían.
- `CommandDialog` se nombra con `labels.dialog` (o `LabelsProvider`, grupo `command`) y el foco inicial cae en el campo.

## Reglas de uso

- **`CommandDialog` para una búsqueda global** que se abre con ⌘K y flota sobre cualquier pantalla; **`Command` incrustado** cuando la búsqueda es parte de la página (un panel lateral, una pantalla de «ir a»). El atajo lo registra la app.
- **Es la búsqueda de iCloud** (2.0): el campo de 36 px con radio 10 y relleno gris, que con el foco pierde el relleno y queda con el anillo interior, y abajo los resultados como las filas de un menú (30 px, radio 8, ícono en el acento). `CommandDialog` es la superficie de un popover: radio 12, opaca, anclada arriba. Hasta R1 era la paleta de Spotlight, con sugerencia en línea y pista `tab`: iCloud no completa en línea.
- **El elegido va en el gris del resaltado de menú, no en el acento.** El primer resultado está elegido desde la primera tecla, y una franja de acento fija gritaría más que los resultados.
- `CommandEmpty` va **al lado** de `CommandList`, no adentro: un listbox solo admite opciones y grupos.
- Los filtros (`CommandFilters`) son los tokens con que iCloud acota una búsqueda: van abajo del campo, el marcado en el acento. No filtran solos: exponen el valor con `onValueChange` y la app decide qué grupos o ítems pasa.
- `shouldFilter={false}` cuando la app ya filtró y ordenó —un ranking propio, una búsqueda en el servidor—: la búsqueda muestra los ítems como vienen.
- Cerrar al elegir lo decide la app en `onSelect`: navegar cierra, pero «copiar el número» puede querer dejarla abierta.

## Relacionados

[autocomplete](/docs/components/autocomplete.md) · [combobox](/docs/components/combobox.md) · [dialog](/docs/components/dialog.md) · [kbd](/docs/components/kbd.md)
