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>
This commit is contained in:
@@ -0,0 +1,204 @@
|
||||
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 }
|
||||
Reference in New Issue
Block a user