Command
La búsqueda de iCloud: un search field y los resultados como filas de menú, con ícono y detalle.
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`.
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.
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
Generadas del TypeScript del paquete. Las propias del componente, más las heredadas del primitivo que tienen algo que explicar —marcadas «heredada de Base UI»—. El resto está en la línea «hereda de».
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. |
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
- Escribir
- Filtra por título y
keywords, sin distinguir mayúsculas ni tildes. - ↑ ↓
- Mueven el elegido.
- Enter
- Ejecuta el
onSelectdel elegido. - Tab
- Sale del campo, como en cualquier combobox: no completa nada.
- Escape
- Cierra
CommandDialog.
Accesibilidad
- El campo es un
comboboxy la lista unlistboxde Base UI (Autocomplete en modoinline): el lector anuncia el elegido mientras el foco se queda en el campo. CommandFilterses unradiogroupnombrado conlabels.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.CommandEmptyes una regiónstatusque queda siempre montada: si apareciera recién con el texto, varios lectores no lo anunciarían.CommandDialogse nombra conlabels.dialog(oLabelsProvider, grupocommand) 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).
CommandDialoges la superficie de un popover: radio 12, opaca, anclada arriba. Hasta R1 era la paleta de Spotlight, con sugerencia en línea y pistatab: 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.
CommandEmptyva al lado deCommandList, 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 cononValueChangey 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.