Ir al contenido
Superposiciones

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>.

PropTipoPor defectoDescripción
defaultValuestring""El valor inicial. Es la versión no controlada de value.
labelsPartial<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.
shouldFilterbooleantrueCon false no filtra: muestra los CommandItem que reciba, en su orden.
valuestring—Lo escrito en el campo. Pasarlo lo vuelve controlado.
classNamestring—Se fusiona con las clases del componente vía cn() (tailwind-merge): lo que pongas gana.

CommandDialog

Hereda las props de Dialog.Root.

PropTipoPor defectoDescripción
defaultValuestring—El valor inicial. Es la versión no controlada de value.
labelsPartial<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.
shouldFilterboolean—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—.
valuestring—Lo escrito en el campo. Pasarlo lo vuelve controlado.
classNamestring—Clases del panel.
defaultOpenboolean—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.
openboolean—Heredada de Base UI. Si está abierto. Pasarla lo vuelve controlado: sin onOpenChange ya no se cierra solo.

CommandEmpty

Hereda las props de <div>.

PropTipoPor defectoDescripción
classNamestring—Se fusiona con las clases del componente vía cn() (tailwind-merge): lo que pongas gana.

CommandFilter

Hereda las props de Radio.Root.

PropTipoPor defectoDescripción
classNamestring—Se fusiona con las clases del componente vía cn() (tailwind-merge): lo que pongas gana.

CommandFilters

Hereda las props de RadioGroup.

PropTipoPor defectoDescripción
onValueChange(value: string) => void—El filtro marcado. Siempre hay uno: tocar el marcado no lo apaga.
classNamestring—Se fusiona con las clases del componente vía cn() (tailwind-merge): lo que pongas gana.

CommandGroup

Hereda las props de Autocomplete.Group.

PropTipoPor defectoDescripción
headingReact.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.
classNamestring—Se fusiona con las clases del componente vía cn() (tailwind-merge): lo que pongas gana.

CommandInput

Hereda las props de Autocomplete.Input.

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

CommandItem

Hereda las props de Autocomplete.Item.

PropTipoPor defectoDescripció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.
descriptionReact.ReactNode—El detalle en gris, en un segundo renglón de 12 px.
iconReact.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.
keywordsreadonly string[]—Palabras que también lo encuentran, además del título.
onClickBaseUIComponentProps<'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.
textValuestring—El título en texto plano, si children no es un string. Es lo que se filtra.
classNamestring—Se fusiona con las clases del componente vía cn() (tailwind-merge): lo que pongas gana.

CommandList

Hereda las props de Autocomplete.List.

PropTipoPor defectoDescripción
classNamestring—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 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