Files
fn_registry/frontend/functions/ui/command.tsx
T
egutierrez 35bcb63300 feat: nuevos componentes UI — accordion, avatar, breadcrumb, checkbox, command, dropdown, pagination, popover, radio, sheet, select, switch, textarea, toast
Componentes React accesibles basados en Radix UI con soporte de temas via CSS variables. Incluye barrel export en index.ts.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 03:23:32 +02:00

205 lines
5.8 KiB
TypeScript

import * as React from "react"
import { SearchIcon, XIcon } from "lucide-react"
import { cn } from "../core/cn"
interface CommandItem {
value: string
label: string
description?: string
icon?: React.ReactNode
disabled?: boolean
group?: string
}
interface CommandProps {
items: CommandItem[]
value?: string
onValueChange?: (value: string) => void
placeholder?: string
emptyMessage?: string
className?: string
inputClassName?: string
listClassName?: string
}
function Command({ className, ...props }: React.ComponentPropsWithoutRef<"div">) {
return (
<div
data-slot="command"
className={cn("flex h-full w-full flex-col overflow-hidden rounded-xl bg-popover text-popover-foreground", className)}
{...props}
/>
)
}
function CommandInput({ className, ...props }: React.ComponentPropsWithoutRef<"input">) {
return (
<div data-slot="command-input-wrapper" className="flex items-center border-b px-3">
<SearchIcon className="mr-2 size-4 shrink-0 text-muted-foreground" />
<input
data-slot="command-input"
className={cn(
"flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-none",
"placeholder:text-muted-foreground",
"disabled:cursor-not-allowed disabled:opacity-50",
className
)}
{...props}
/>
</div>
)
}
function CommandList({ className, ...props }: React.ComponentPropsWithoutRef<"div">) {
return (
<div
data-slot="command-list"
className={cn("max-h-[300px] scroll-py-1 overflow-x-hidden overflow-y-auto", className)}
{...props}
/>
)
}
function CommandEmpty({ className, ...props }: React.ComponentPropsWithoutRef<"div">) {
return (
<div
data-slot="command-empty"
className={cn("py-6 text-center text-sm text-muted-foreground", className)}
{...props}
/>
)
}
function CommandGroup({ className, heading, ...props }: React.ComponentPropsWithoutRef<"div"> & { heading?: string }) {
return (
<div data-slot="command-group" className={cn("overflow-hidden p-1 text-foreground", className)}>
{heading && (
<div className="px-2 py-1.5 text-xs font-medium text-muted-foreground">{heading}</div>
)}
<div {...props} />
</div>
)
}
function CommandSeparator({ className, ...props }: React.ComponentPropsWithoutRef<"div">) {
return (
<div
data-slot="command-separator"
className={cn("-mx-1 h-px bg-border", className)}
{...props}
/>
)
}
interface CommandItemProps extends React.ComponentPropsWithoutRef<"div"> {
selected?: boolean
disabled?: boolean
onSelect?: () => void
}
function CommandItem({ className, selected, disabled, onSelect, ...props }: CommandItemProps) {
return (
<div
data-slot="command-item"
data-selected={selected}
aria-disabled={disabled}
role="option"
aria-selected={selected}
onClick={!disabled ? onSelect : undefined}
className={cn(
"relative flex cursor-default select-none items-center gap-2 rounded-md px-2 py-1.5 text-sm outline-none",
"data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground",
"hover:bg-accent hover:text-accent-foreground",
disabled && "pointer-events-none opacity-50",
className
)}
{...props}
/>
)
}
function CommandShortcut({ className, ...props }: React.ComponentPropsWithoutRef<"span">) {
return (
<span
data-slot="command-shortcut"
className={cn("ml-auto text-xs tracking-widest text-muted-foreground", className)}
{...props}
/>
)
}
function CommandSearch({
items,
value,
onValueChange,
placeholder = "Search...",
emptyMessage = "No results found.",
className,
}: CommandProps) {
const [query, setQuery] = React.useState("")
const [selectedValue, setSelectedValue] = React.useState(value ?? "")
const filtered = React.useMemo(() => {
if (!query) return items
const q = query.toLowerCase()
return items.filter(
(item) =>
item.label.toLowerCase().includes(q) ||
item.description?.toLowerCase().includes(q) ||
item.value.toLowerCase().includes(q)
)
}, [items, query])
const groups = React.useMemo(() => {
const map = new Map<string, CommandItem[]>()
for (const item of filtered) {
const key = item.group ?? ""
if (!map.has(key)) map.set(key, [])
map.get(key)!.push(item)
}
return map
}, [filtered])
const handleSelect = (val: string) => {
setSelectedValue(val)
onValueChange?.(val)
}
return (
<Command className={className}>
<CommandInput
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder={placeholder}
/>
<CommandList>
{filtered.length === 0 ? (
<CommandEmpty>{emptyMessage}</CommandEmpty>
) : (
Array.from(groups.entries()).map(([group, groupItems]) => (
<CommandGroup key={group} heading={group || undefined}>
{groupItems.map((item) => (
<CommandItem
key={item.value}
selected={selectedValue === item.value}
disabled={item.disabled}
onSelect={() => handleSelect(item.value)}
>
{item.icon && <span className="shrink-0">{item.icon}</span>}
<span>{item.label}</span>
{item.description && (
<span className="ml-auto text-xs text-muted-foreground">{item.description}</span>
)}
</CommandItem>
))}
</CommandGroup>
))
)}
</CommandList>
</Command>
)
}
export { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, CommandSearch, CommandSeparator, CommandShortcut }
export type { CommandItem, CommandProps }