diff --git a/web/src/components/ui/shadcn-io/combobox/index.tsx b/web/src/components/ui/shadcn-io/combobox/index.tsx new file mode 100644 index 00000000..4a772a63 --- /dev/null +++ b/web/src/components/ui/shadcn-io/combobox/index.tsx @@ -0,0 +1,311 @@ +'use client'; + +import { useControllableState } from '@radix-ui/react-use-controllable-state'; +import { ChevronsUpDownIcon, PlusIcon } from 'lucide-react'; +import { + type ComponentProps, + createContext, + type ReactNode, + useContext, + useEffect, + useRef, + useState, +} from 'react'; +import { Button } from '@/components/ui/button'; +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, + CommandSeparator, +} from '@/components/ui/command'; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from '@/components/ui/popover'; +import { cn } from '@/lib/utils'; + +type ComboboxData = { + label: string; + value: string; +}; + +type ComboboxContextType = { + data: ComboboxData[]; + type: string; + value: string; + onValueChange: (value: string) => void; + open: boolean; + onOpenChange: (open: boolean) => void; + width: number; + setWidth: (width: number) => void; + inputValue: string; + setInputValue: (value: string) => void; +}; + +const ComboboxContext = createContext({ + data: [], + type: 'item', + value: '', + onValueChange: () => {}, + open: false, + onOpenChange: () => {}, + width: 200, + setWidth: () => {}, + inputValue: '', + setInputValue: () => {}, +}); + +export type ComboboxProps = ComponentProps & { + data: ComboboxData[]; + type: string; + defaultValue?: string; + value?: string; + onValueChange?: (value: string) => void; + open?: boolean; + onOpenChange?: (open: boolean) => void; +}; + +export const Combobox = ({ + data, + type, + defaultValue, + value: controlledValue, + onValueChange: controlledOnValueChange, + defaultOpen = false, + open: controlledOpen, + onOpenChange: controlledOnOpenChange, + ...props +}: ComboboxProps) => { + const [value, onValueChange] = useControllableState({ + defaultProp: defaultValue ?? '', + prop: controlledValue, + onChange: controlledOnValueChange, + }); + const [open, onOpenChange] = useControllableState({ + defaultProp: defaultOpen, + prop: controlledOpen, + onChange: controlledOnOpenChange, + }); + const [width, setWidth] = useState(200); + const [inputValue, setInputValue] = useState(''); + + return ( + + + + ); +}; + +export type ComboboxTriggerProps = ComponentProps; + +export const ComboboxTrigger = ({ + children, + ...props +}: ComboboxTriggerProps) => { + const { value, data, type, setWidth } = useContext(ComboboxContext); + const ref = useRef(null); + + useEffect(() => { + // Create a ResizeObserver to detect width changes + const resizeObserver = new ResizeObserver((entries) => { + for (const entry of entries) { + const newWidth = (entry.target as HTMLElement).offsetWidth; + if (newWidth) { + setWidth?.(newWidth); + } + } + }); + + if (ref.current) { + resizeObserver.observe(ref.current); + } + + // Clean up the observer when component unmounts + return () => { + resizeObserver.disconnect(); + }; + }, [setWidth]); + + return ( + + + + ); +}; + +export type ComboboxContentProps = ComponentProps & { + popoverOptions?: ComponentProps; +}; + +export const ComboboxContent = ({ + className, + popoverOptions, + ...props +}: ComboboxContentProps) => { + const { width } = useContext(ComboboxContext); + + return ( + + + + ); +}; + +export type ComboboxInputProps = ComponentProps & { + value?: string; + defaultValue?: string; + onValueChange?: (value: string) => void; +}; + +export const ComboboxInput = ({ + value: controlledValue, + defaultValue, + onValueChange: controlledOnValueChange, + ...props +}: ComboboxInputProps) => { + const { type, inputValue, setInputValue } = useContext(ComboboxContext); + + const [value, onValueChange] = useControllableState({ + defaultProp: defaultValue ?? inputValue, + prop: controlledValue, + onChange: (newValue) => { + // Sync with context state + setInputValue(newValue); + // Call external onChange if provided + controlledOnValueChange?.(newValue); + }, + }); + + return ( + + ); +}; + +export type ComboboxListProps = ComponentProps; + +export const ComboboxList = (props: ComboboxListProps) => ( + +); + +export type ComboboxEmptyProps = ComponentProps; + +export const ComboboxEmpty = ({ children, ...props }: ComboboxEmptyProps) => { + const { type } = useContext(ComboboxContext); + + return ( + {children ?? `No ${type} found.`} + ); +}; + +export type ComboboxGroupProps = ComponentProps; + +export const ComboboxGroup = (props: ComboboxGroupProps) => ( + +); + +export type ComboboxItemProps = ComponentProps; + +export const ComboboxItem = (props: ComboboxItemProps) => { + const { onValueChange, onOpenChange } = useContext(ComboboxContext); + + return ( + { + onValueChange(currentValue); + onOpenChange(false); + }} + {...props} + /> + ); +}; + +export type ComboboxSeparatorProps = ComponentProps; + +export const ComboboxSeparator = (props: ComboboxSeparatorProps) => ( + +); + +export type ComboboxCreateNewProps = { + onCreateNew: (value: string) => void; + children?: (inputValue: string) => ReactNode; + className?: string; +}; + +export const ComboboxCreateNew = ({ + onCreateNew, + children, + className, +}: ComboboxCreateNewProps) => { + const { inputValue, type, onValueChange, onOpenChange } = + useContext(ComboboxContext); + + if (!inputValue.trim()) { + return null; + } + + const handleCreateNew = () => { + onCreateNew(inputValue.trim()); + onValueChange(inputValue.trim()); + onOpenChange(false); + }; + + return ( + + ); +};