| 'use client'; |
|
|
| import * as DropdownMenu from '@radix-ui/react-dropdown-menu'; |
| import { motion, AnimatePresence } from 'framer-motion'; |
| import { cn } from '@/lib/utils'; |
| import { useState } from 'react'; |
|
|
| export interface GlassSelectProps { |
| value: string; |
| onChange: (value: string) => void; |
| options: string[]; |
| placeholder?: string; |
| label?: string; |
| disabled?: boolean; |
| isLoading?: boolean; |
| className?: string; |
| } |
|
|
| export function GlassSelect({ |
| value, |
| onChange, |
| options, |
| placeholder = 'Select...', |
| label, |
| disabled = false, |
| isLoading = false, |
| className, |
| }: GlassSelectProps) { |
| const [open, setOpen] = useState(false); |
|
|
| return ( |
| <div className={cn('flex flex-col gap-1.5', className)}> |
| {label && ( |
| <label className="text-sm font-medium text-[var(--foreground)]/70"> |
| {label} |
| </label> |
| )} |
| |
| <DropdownMenu.Root open={open} onOpenChange={setOpen}> |
| <DropdownMenu.Trigger asChild disabled={disabled || isLoading}> |
| <button |
| className={cn( |
| 'flex items-center justify-between w-full px-4 py-2.5 rounded-xl', |
| 'glass text-left', |
| 'focus-ring', |
| 'disabled:opacity-50 disabled:cursor-not-allowed', |
| 'transition-all duration-200' |
| )} |
| > |
| <span |
| className={cn( |
| 'truncate', |
| !value && 'text-[var(--foreground)]/40' |
| )} |
| > |
| {isLoading ? 'Loading models...' : value || placeholder} |
| </span> |
| <svg |
| className={cn( |
| 'w-4 h-4 ml-2 transition-transform duration-200', |
| open && 'rotate-180' |
| )} |
| fill="none" |
| viewBox="0 0 24 24" |
| stroke="currentColor" |
| > |
| <path |
| strokeLinecap="round" |
| strokeLinejoin="round" |
| strokeWidth={2} |
| d="M19 9l-7 7-7-7" |
| /> |
| </svg> |
| </button> |
| </DropdownMenu.Trigger> |
| |
| <AnimatePresence> |
| {open && ( |
| <DropdownMenu.Portal forceMount> |
| <DropdownMenu.Content |
| asChild |
| sideOffset={8} |
| align="start" |
| className="z-50" |
| > |
| <motion.div |
| initial={{ opacity: 0, y: -8, scale: 0.96 }} |
| animate={{ opacity: 1, y: 0, scale: 1 }} |
| exit={{ opacity: 0, y: -8, scale: 0.96 }} |
| transition={{ duration: 0.15, ease: [0.25, 0.46, 0.45, 0.94] }} |
| className={cn( |
| 'w-[var(--radix-dropdown-menu-trigger-width)] max-h-64 overflow-y-auto', |
| 'glass-elevated rounded-xl p-1', |
| 'shadow-xl' |
| )} |
| > |
| {options.length === 0 ? ( |
| <div className="px-3 py-2 text-sm text-[var(--foreground)]/50"> |
| No models available |
| </div> |
| ) : ( |
| options.map((option) => ( |
| <DropdownMenu.Item |
| key={option} |
| className={cn( |
| 'flex items-center justify-between px-3 py-2 rounded-lg', |
| 'text-sm cursor-pointer', |
| 'outline-none', |
| 'hover:bg-[var(--glass-bg-muted)]', |
| 'focus:bg-[var(--glass-bg-muted)]', |
| 'transition-colors duration-150', |
| option === value && 'bg-[var(--suggestion-bg)]' |
| )} |
| onSelect={() => onChange(option)} |
| > |
| <span className="truncate">{option}</span> |
| {option === value && ( |
| <svg |
| className="w-4 h-4 text-[var(--suggestion-accent)]" |
| fill="none" |
| viewBox="0 0 24 24" |
| stroke="currentColor" |
| > |
| <path |
| strokeLinecap="round" |
| strokeLinejoin="round" |
| strokeWidth={2} |
| d="M5 13l4 4L19 7" |
| /> |
| </svg> |
| )} |
| </DropdownMenu.Item> |
| )) |
| )} |
| </motion.div> |
| </DropdownMenu.Content> |
| </DropdownMenu.Portal> |
| )} |
| </AnimatePresence> |
| </DropdownMenu.Root> |
| </div> |
| ); |
| } |
|
|