clinicpal / src /components /ui /glass-select.tsx
Vrda's picture
Deploy ClinIcPal frontend
9bc2f29 verified
'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>
);
}