Better code quality

This commit is contained in:
2025-10-04 14:52:20 +02:00
parent c6cc1a47c0
commit ba37b94908
25 changed files with 904 additions and 588 deletions

118
src/components/Knob.tsx Normal file
View File

@ -0,0 +1,118 @@
import { useRef, useState, useEffect } from 'react'
interface KnobProps {
label: string
value: number
min: number
max: number
step: number
unit?: string
onChange: (value: number) => void
formatValue?: (id: string, value: number) => string
valueId?: string
size?: number
}
export function Knob({
label,
value,
min,
max,
step,
unit,
onChange,
formatValue,
valueId,
size = 48
}: KnobProps) {
const [isDragging, setIsDragging] = useState(false)
const startYRef = useRef<number>(0)
const startValueRef = useRef<number>(0)
const displayValue = formatValue && valueId ? formatValue(valueId, value) : `${value}${unit || ''}`
const normalizedValue = (value - min) / (max - min)
const angle = -225 + normalizedValue * 270
const handleMouseDown = (e: React.MouseEvent) => {
setIsDragging(true)
startYRef.current = e.clientY
startValueRef.current = value
e.preventDefault()
}
const handleMouseMove = (e: MouseEvent) => {
if (!isDragging) return
const deltaY = startYRef.current - e.clientY
const range = max - min
const sensitivity = range / 200
const newValue = Math.max(min, Math.min(max, startValueRef.current + deltaY * sensitivity))
const steppedValue = Math.round(newValue / step) * step
onChange(steppedValue)
}
const handleMouseUp = () => {
setIsDragging(false)
}
useEffect(() => {
if (isDragging) {
window.addEventListener('mousemove', handleMouseMove)
window.addEventListener('mouseup', handleMouseUp)
return () => {
window.removeEventListener('mousemove', handleMouseMove)
window.removeEventListener('mouseup', handleMouseUp)
}
}
}, [isDragging])
return (
<div className="relative flex flex-col items-center">
<div
className="relative cursor-ns-resize select-none"
onMouseDown={handleMouseDown}
style={{ width: size, height: size }}
>
<svg
width={size}
height={size}
viewBox={`0 0 ${size} ${size}`}
>
<circle
cx={size / 2}
cy={size / 2}
r={(size - 4) / 2}
fill="none"
stroke="white"
strokeWidth="2"
/>
<circle
cx={size / 2}
cy={size / 2}
r={(size - 8) / 2}
fill="black"
/>
<line
x1={size / 2 + Math.cos((angle * Math.PI) / 180) * ((size - 16) / 2)}
y1={size / 2 + Math.sin((angle * Math.PI) / 180) * ((size - 16) / 2)}
x2={size / 2 + Math.cos((angle * Math.PI) / 180) * ((size - 4) / 2)}
y2={size / 2 + Math.sin((angle * Math.PI) / 180) * ((size - 4) / 2)}
stroke="white"
strokeWidth="2"
strokeLinecap="square"
/>
</svg>
<div className="absolute inset-0 flex items-center justify-center pointer-events-none">
<span className="font-mono text-[9px] tracking-[0.15em] text-white">
{isDragging ? displayValue : label.toUpperCase()}
</span>
</div>
</div>
</div>
)
}