({
pattern,
setPattern,
threshold,
setThreshold,
invert,
setInvert,
serpentine,
setSerpentine,
paletteId,
groupByCategory = false,
defaultOpen = true,
})
| 23 | }; |
| 24 | |
| 25 | const AlgorithmSelect: React.FC<AlgorithmSelectProps> = ({ |
| 26 | pattern, |
| 27 | setPattern, |
| 28 | threshold, |
| 29 | setThreshold, |
| 30 | invert, |
| 31 | setInvert, |
| 32 | serpentine, |
| 33 | setSerpentine, |
| 34 | paletteId, |
| 35 | groupByCategory = false, |
| 36 | defaultOpen = true, |
| 37 | }) => { |
| 38 | const { t } = useTranslation(); |
| 39 | const [open, setOpen] = useState(defaultOpen); |
| 40 | |
| 41 | const algoMetaMap: Record<number, { description: string; supportsThreshold: boolean; category: string; name: string }> = {}; |
| 42 | for (const a of algorithms) { algoMetaMap[a.id] = { description: a.name, supportsThreshold: a.supportsThreshold, category: a.category, name: a.name }; } |
| 43 | |
| 44 | const renderOptions = () => { |
| 45 | if (!groupByCategory) { |
| 46 | return algorithms.map(a => <option key={a.id} value={a.id}>{a.name}</option>); |
| 47 | } |
| 48 | const categoryOrder = ["Error Diffusion", "Ordered", "Other"]; |
| 49 | const groups = getAlgorithmsByCategory(); |
| 50 | return categoryOrder.map(cat => { |
| 51 | const list = groups[cat]; if (!list || !list.length) return null; |
| 52 | const labelKey = cat === 'Error Diffusion' ? 'tool.algorithmPanel.errorDiffusion' : cat === 'Ordered' ? 'tool.algorithmPanel.ordered' : 'tool.algorithmPanel.other'; |
| 53 | return <optgroup key={cat} label={t(labelKey)}>{list.map(a => <option key={a.id} value={a.id}>{a.name}</option>)}</optgroup>; |
| 54 | }); |
| 55 | }; |
| 56 | |
| 57 | return ( |
| 58 | <div className="min-panel p-0"> |
| 59 | <button type="button" onClick={() => setOpen(o => !o)} className="flex w-full items-center justify-between px-4 py-3 text-left font-mono text-[11px] tracking-wide text-gray-300 hover:bg-neutral-800/40 focus-visible:shadow-[var(--focus-ring)]" aria-expanded={open}> |
| 60 | <span className="flex items-center gap-2"><span>{open ? '▾' : '▸'}</span> {t('tool.algorithmPanel.title')}</span> |
| 61 | <span className="text-[10px] text-gray-500">#{pattern}</span> |
| 62 | </button> |
| 63 | {open && ( |
| 64 | <div className="space-y-3 border-t border-neutral-800 px-4 pt-3 pb-4"> |
| 65 | <div> |
| 66 | <label htmlFor="algo-select" className="sr-only">{t('tool.algorithmPanel.title')}</label> |
| 67 | <select id="algo-select" className="clean-input" value={pattern} onChange={(e) => setPattern(Number(e.target.value))}> |
| 68 | {renderOptions()} |
| 69 | </select> |
| 70 | </div> |
| 71 | {algoMetaMap[pattern]?.supportsThreshold && ( |
| 72 | <div className="space-y-1"> |
| 73 | <div className="flex items-center justify-between"> |
| 74 | <span className="font-mono text-[10px] tracking-wide text-gray-400">{t('tool.algorithmPanel.threshold')}</span> |
| 75 | <span className="text-[10px] text-gray-500">{threshold}</span> |
| 76 | </div> |
| 77 | <input type="range" min={0} max={255} value={threshold} className="clean-range" onChange={(e) => setThreshold(Number(e.target.value))} aria-label={t('tool.algorithmPanel.threshold')} /> |
| 78 | </div> |
| 79 | )} |
| 80 | <div className="grid grid-cols-2 gap-2"> |
| 81 | <button type="button" onClick={() => !paletteId && setInvert(v => !v)} disabled={!!paletteId} className={`clean-btn justify-center text-[10px] ${invert ? 'border-emerald-600 text-emerald-400' : ''} ${paletteId ? 'cursor-not-allowed opacity-40' : ''}`}>{t('tool.algorithmPanel.invert')}</button> |
| 82 | <button type="button" onClick={() => setSerpentine(s => !s)} className={`clean-btn justify-center text-[10px] ${serpentine ? 'border-blue-600 text-blue-400' : ''}`}>{t('tool.algorithmPanel.serpentine')}</button> |
nothing calls this directly
no test coverage detected