"use client"; import { useId, useState, useRef } from "react"; import { Dialog } from "../composite/Dialog.tsx"; export type Command = { id: string; label: string; description?: string; shortcut?: string; onSelect: () => void; disabled?: boolean; }; export function CommandPalette({ open, onOpenChange, commands, label = "Command palette", placeholder = "What would you like to do?", className = "", }: { open: boolean; onOpenChange: (open: boolean) => void; commands: readonly Command[]; label?: string; placeholder?: string; className?: string; }) { const [query, setQuery] = useState(""), [cursor, setCursor] = useState(0); const list = useRef(null), id = useId(); const matches = commands.filter( (c) => !c.disabled && `${c.label} ${c.description ?? ""}` .toLowerCase() .includes(query.toLowerCase()), ); const active = Math.min(cursor, Math.max(0, matches.length - 1)); const select = (command: Command) => { onOpenChange(false); setQuery(""); setCursor(0); command.onSelect(); }; return ( { setQuery(e.target.value); setCursor(0); }} onKeyDown={(e) => { if (e.key === "ArrowDown" || e.key === "ArrowUp") { e.preventDefault(); const next = matches.length ? (active + (e.key === "ArrowDown" ? 1 : -1) + matches.length) % matches.length : 0; setCursor(next); list.current?.children[next]?.scrollIntoView({ block: "nearest" }); } if (e.key === "Enter" && matches[active]) { e.preventDefault(); select(matches[active]!); } }} />
{matches.map((c, i) => (
setCursor(i)} onClick={() => select(c)} >
{c.label} {c.description && {c.description}}
{c.shortcut && {c.shortcut}}
))}
{!matches.length && (

No matching commands.

)}

↑ ↓ to navigate · Enter to choose · Escape to close

); }