Spaces:
Running
Running
| 'use client'; | |
| /** | |
| * @license | |
| * SPDX-License-Identifier: Apache-2.0 | |
| */ | |
| import React from 'react'; | |
| import { Info } from 'lucide-react'; | |
| import { motion, AnimatePresence } from 'framer-motion'; | |
| interface SearchSelectProps { | |
| label: string; | |
| value: string; | |
| options: string[]; | |
| onChange: (value: string) => void; | |
| className?: string; | |
| } | |
| export function SearchSelect({ label, value, options, onChange, className = "" }: SearchSelectProps) { | |
| return ( | |
| <div className={`space-y-4 ${className}`}> | |
| <label className="text-sm font-bold text-slate-400 uppercase tracking-[0.1em] block"> | |
| {label} | |
| </label> | |
| <div className="relative"> | |
| <select | |
| value={value} | |
| onChange={e => onChange(e.target.value)} | |
| className="w-full h-14 px-5 bg-slate-50 border border-slate-100 rounded-2xl text-slate-700 text-base font-bold appearance-none focus:bg-white focus:border-cyan-500 focus:ring-4 focus:ring-cyan-50/50 transition-all outline-none" | |
| > | |
| <option value="" disabled hidden>선택해주세요</option> | |
| {options.map(opt => <option key={opt} value={opt} className="text-base">{opt}</option>)} | |
| </select> | |
| <div className="absolute right-5 top-1/2 -translate-y-1/2 pointer-events-none text-slate-400"> | |
| <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | |
| <path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2.5" d="M19 9l-7 7-7-7" /> | |
| </svg> | |
| </div> | |
| </div> | |
| </div> | |
| ); | |
| } | |
| interface SearchRadioGroupProps { | |
| label: string; | |
| value: string; | |
| options: string[]; | |
| onChange: (value: string) => void; | |
| name: string; | |
| } | |
| export function SearchRadioGroup({ label, value, options, onChange, name }: SearchRadioGroupProps) { | |
| return ( | |
| <div className="space-y-5"> | |
| <label className="text-sm font-bold text-slate-400 uppercase tracking-[0.1em] block"> | |
| {label} | |
| </label> | |
| <div className="grid grid-cols-2 md:grid-cols-4 gap-3 sm:gap-5"> | |
| {options.map(opt => ( | |
| <label | |
| key={opt} | |
| className={` | |
| flex items-center justify-center min-h-14 px-4 py-3 rounded-2xl text-sm sm:text-base text-center break-keep font-bold border transition-all cursor-pointer | |
| ${value === opt | |
| ? 'border-cyan-500 bg-cyan-50 text-cyan-700 ring-4 ring-cyan-50' | |
| : 'border-slate-100 bg-slate-50 text-slate-400 hover:border-slate-200'} | |
| `} | |
| > | |
| <input | |
| type="radio" | |
| name={name} | |
| value={opt} | |
| checked={value === opt} | |
| onChange={() => onChange(opt)} | |
| className="sr-only" | |
| /> | |
| {opt} | |
| </label> | |
| ))} | |
| </div> | |
| </div> | |
| ); | |
| } | |
| interface SearchInputProps { | |
| label: string; | |
| value: number; | |
| onChange: (value: number) => void; | |
| unit: string; | |
| error?: string; | |
| } | |
| export function SearchInput({ label, value, onChange, unit, error }: SearchInputProps) { | |
| return ( | |
| <div className="space-y-4"> | |
| <div className="flex items-center justify-between"> | |
| <label className="text-sm font-bold text-slate-400 uppercase tracking-[0.1em]"> | |
| {label} | |
| </label> | |
| </div> | |
| <div className="relative"> | |
| <input | |
| type="number" | |
| value={value === 0 ? '' : value} | |
| onChange={e => onChange(Number(e.target.value))} | |
| className="w-full h-14 pl-5 pr-16 bg-slate-50 border border-slate-100 rounded-2xl text-base font-bold text-slate-800 focus:bg-white focus:border-cyan-500 focus:ring-4 focus:ring-cyan-50/50 transition-all outline-none" | |
| placeholder="0" | |
| /> | |
| <div className="absolute right-5 top-1/2 -translate-y-1/2 pointer-events-none font-bold text-slate-400">{unit}</div> | |
| </div> | |
| {error && ( | |
| <div className="text-[12px] font-bold text-rose-500 animate-pulse px-1"> | |
| {error} | |
| </div> | |
| )} | |
| </div> | |
| ); | |
| } | |
| interface InfoGuideProps { | |
| activeKey: string; | |
| data: Record<string, React.ReactNode>; | |
| className?: string; | |
| } | |
| export function InfoGuide({ activeKey, data, className = "" }: InfoGuideProps) { | |
| const content = data[activeKey]; | |
| return ( | |
| <div className={`min-h-[32px] flex items-start space-x-2 mt-3 px-1 ${className}`}> | |
| <Info className="w-4 h-4 text-slate-400 mt-1 shrink-0" /> | |
| <div className="text-[13px] sm:text-[14px] text-slate-600 leading-relaxed font-medium flex-grow"> | |
| <AnimatePresence mode="wait"> | |
| {content && ( | |
| <motion.div | |
| key={activeKey} | |
| initial={{ opacity: 0, x: -5 }} | |
| animate={{ opacity: 1, x: 0 }} | |
| exit={{ opacity: 0, x: 5 }} | |
| transition={{ duration: 0.2 }} | |
| > | |
| {content} | |
| </motion.div> | |
| )} | |
| </AnimatePresence> | |
| </div> | |
| </div> | |
| ); | |
| } | |