({ pattern, setPattern, threshold, setThreshold, invert, setInvert, asciiRamp, setAsciiRamp })
| 17 | } |
| 18 | |
| 19 | const AlgorithmPanel: React.FC<AlgorithmPanelProps> = ({ pattern, setPattern, threshold, setThreshold, invert, setInvert, asciiRamp, setAsciiRamp }) => { |
| 20 | const { t } = useTranslation(); |
| 21 | const algoObj = algorithms.find(a=>a.id===pattern); |
| 22 | const isAscii = pattern === 25 || algoObj?.name.toLowerCase().includes('ascii'); |
| 23 | const normalizedRamp = useMemo(()=>{ |
| 24 | if (!asciiRamp) return DEFAULT_ASCII_RAMP; return Array.from(new Set(asciiRamp.split('').filter(ch=>ch !== '\n' && ch!=='\r'))).join('').slice(0,128); |
| 25 | },[asciiRamp]); |
| 26 | |
| 27 | return ( |
| 28 | <CollapsiblePanel |
| 29 | title={t(isAscii ? 'tool.algorithmPanel.titleAscii' : 'tool.algorithmPanel.title')} |
| 30 | subtitle={`#${pattern}`} |
| 31 | defaultOpen={true} |
| 32 | > |
| 33 | <div className="space-y-3"> |
| 34 | <div> |
| 35 | <label htmlFor="algo-select" className="sr-only">{t('tool.algorithmPanel.title')}</label> |
| 36 | <select id="algo-select" className="clean-input" value={pattern} onChange={(e) => setPattern(Number(e.target.value))}> |
| 37 | {(() => { |
| 38 | const groups = getAlgorithmsByCategory(); |
| 39 | const order: ("Error Diffusion"|"Ordered"|"Other")[] = ["Error Diffusion","Ordered","Other"]; |
| 40 | return order.map(cat => { |
| 41 | const list = groups[cat]; if (!list) return null; |
| 42 | const labelKey = cat === 'Error Diffusion' ? 'tool.algorithmPanel.errorDiffusion' : cat === 'Ordered' ? 'tool.algorithmPanel.ordered' : 'tool.algorithmPanel.other'; |
| 43 | return <optgroup key={cat} label={t(labelKey)}>{list.map(a => <option key={a.id} value={a.id}>{a.name}</option>)}</optgroup>; |
| 44 | }); |
| 45 | })()} |
| 46 | </select> |
| 47 | </div> |
| 48 | {algorithms.find(a=>a.id===pattern)?.supportsThreshold && ( |
| 49 | <div className="space-y-1"> |
| 50 | <div className="flex items-center justify-between"><span className="font-mono text-[10px] tracking-wide text-gray-400">{t('tool.algorithmPanel.luminanceThreshold')}</span><span className="text-[10px] text-gray-500">{threshold}</span></div> |
| 51 | <input type="range" min={0} max={255} value={threshold} className="clean-range" onChange={(e) => setThreshold(Number(e.target.value))} aria-label={t('tool.algorithmPanel.luminanceThreshold')} /> |
| 52 | </div> |
| 53 | )} |
| 54 | <div className="grid grid-cols-1 gap-2"> |
| 55 | <button |
| 56 | type="button" |
| 57 | onClick={() => setInvert(v => !v)} |
| 58 | className={`clean-btn justify-center text-[10px] ${invert ? 'border-blue-600 text-blue-400' : ''}`} |
| 59 | >{t('tool.algorithmPanel.invert')}</button> |
| 60 | </div> |
| 61 | {isAscii && setAsciiRamp && ( |
| 62 | <div className="space-y-2" data-section="ascii-ramp"> |
| 63 | <div className="flex items-center justify-between"> |
| 64 | <span className="font-mono text-[10px] tracking-wide text-gray-400">{t('tool.algorithmPanel.asciiRamp')}</span> |
| 65 | <span className="text-[9px] text-gray-500">{normalizedRamp.length} {t('tool.algorithmPanel.chars')}</span> |
| 66 | </div> |
| 67 | <textarea |
| 68 | value={asciiRamp || DEFAULT_ASCII_RAMP} |
| 69 | onChange={(e)=> { |
| 70 | const raw = e.target.value.replace(/\s+/g,' '); // collapse whitespace |
| 71 | const uniq = Array.from(new Set(raw.split(''))).join(''); |
| 72 | setAsciiRamp(uniq.slice(0,128)); |
| 73 | }} |
| 74 | rows={2} |
| 75 | className="clean-input !px-2 !py-1 font-mono text-[11px] resize-none" |
| 76 | aria-label={t('tool.algorithmPanel.asciiRamp')} |
nothing calls this directly
no test coverage detected