Files
chaos-api/web/default/src/components/multi-select.tsx
T
t0ng7u 948780e3fa 🎨 fix(theme): align UI controls with global radius tokens
Remove hard-coded and capped border radius overrides so shared controls and feature actions consistently follow the active theme radius.

- Replace fixed radius utilities with semantic theme-aware radius tokens
- Remove redundant `rounded-full` and pixel-based overrides from header, toolbar, Playground, and utility actions
- Drop unused `StatusBadge` rounded prop usage
- Keep existing component behavior intact while improving global theme consistency
2026-05-08 01:50:03 +08:00

133 lines
4.2 KiB
TypeScript
Vendored

import * as React from 'react'
import { Command as CommandPrimitive } from 'cmdk'
import { X } from 'lucide-react'
import { useTranslation } from 'react-i18next'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import { Command, CommandGroup, CommandItem } from '@/components/ui/command'
export type Option = {
label: string
value: string
}
interface MultiSelectProps {
options: Option[]
selected: string[]
onChange: (values: string[]) => void
placeholder?: string
className?: string
}
export function MultiSelect({
options,
selected,
onChange,
placeholder,
className,
}: MultiSelectProps) {
const { t } = useTranslation()
const resolvedPlaceholder = placeholder ?? t('Select items...')
const inputRef = React.useRef<HTMLInputElement>(null)
const [open, setOpen] = React.useState(false)
const [inputValue, setInputValue] = React.useState('')
const handleUnselect = (value: string) => {
onChange(selected.filter((s) => s !== value))
}
const handleKeyDown = (e: React.KeyboardEvent<HTMLDivElement>) => {
const input = inputRef.current
if (input) {
if (e.key === 'Delete' || e.key === 'Backspace') {
if (input.value === '' && selected.length > 0) {
onChange(selected.slice(0, -1))
}
}
if (e.key === 'Escape') {
input.blur()
}
}
}
const selectables = options.filter(
(option) => !selected.includes(option.value)
)
return (
<Command
onKeyDown={handleKeyDown}
className={`overflow-visible bg-transparent ${className || ''}`}
>
<div className='group border-input ring-offset-background focus-within:ring-ring rounded-md border px-3 py-2 text-sm focus-within:ring-2 focus-within:ring-offset-2'>
<div className='flex flex-wrap gap-1'>
{selected.map((value) => {
const option = options.find((o) => o.value === value)
return (
<Badge key={value} variant='secondary'>
{option?.label || value}
<Button
variant='ghost'
size='icon-sm'
aria-label='Remove'
className='ml-1 size-auto p-0'
onKeyDown={(e) => {
if (e.key === 'Enter') {
handleUnselect(value)
}
}}
onMouseDown={(e) => {
e.preventDefault()
e.stopPropagation()
}}
onClick={() => handleUnselect(value)}
>
<X
className='text-muted-foreground hover:text-foreground h-3 w-3'
aria-hidden='true'
/>
</Button>
</Badge>
)
})}
<CommandPrimitive.Input
ref={inputRef}
value={inputValue}
onValueChange={setInputValue}
onBlur={() => setOpen(false)}
onFocus={() => setOpen(true)}
placeholder={selected.length === 0 ? resolvedPlaceholder : ''}
className='placeholder:text-muted-foreground flex-1 bg-transparent outline-none'
/>
</div>
</div>
<div className='relative'>
{open && selectables.length > 0 ? (
<div className='bg-popover text-popover-foreground animate-in absolute top-0 z-10 w-full rounded-md border shadow-md outline-none'>
<CommandGroup className='h-full max-h-60 overflow-auto'>
{selectables.map((option) => {
return (
<CommandItem
key={option.value}
onMouseDown={(e) => {
e.preventDefault()
e.stopPropagation()
}}
onSelect={() => {
setInputValue('')
onChange([...selected, option.value])
}}
className='cursor-pointer'
>
{option.label}
</CommandItem>
)
})}
</CommandGroup>
</div>
) : null}
</div>
</Command>
)
}