import { useCallback, useEffect, useRef, useState } from 'react'; interface DeviceSliderProps { min: number; max: number; value: number; onChange: (v: number) => void; } const pctOf = (v: number, min: number, max: number) => (v - min) / (max - min); export function DeviceSlider({ min, max, value, onChange }: DeviceSliderProps) { const wrapRef = useRef(null); const frameRef = useRef(null); const hitRef = useRef(null); const pointerRef = useRef(null); const haloRef = useRef(null); const fillRef = useRef(null); const [dragging, setDragging] = useState(false); const renderPositions = useCallback(() => { const frame = frameRef.current; if (!frame) return; const innerW = Math.max(0, frame.clientWidth - 8); const x = 4 + pctOf(value, min, max) * innerW; if (pointerRef.current) pointerRef.current.style.left = `${x}px`; if (haloRef.current) haloRef.current.style.left = `${x}px`; if (fillRef.current) fillRef.current.style.width = `${x - 4}px`; }, [max, min, value]); useEffect(() => { renderPositions(); }, [renderPositions]); useEffect(() => { window.addEventListener('resize', renderPositions); return () => window.removeEventListener('resize', renderPositions); }, [renderPositions]); const valueFromClientX = useCallback( (clientX: number) => { const frame = frameRef.current; if (!frame) return value; const rect = frame.getBoundingClientRect(); const innerLeft = rect.left + 4; const innerW = rect.width - 8; const p = (clientX - innerLeft) / innerW; const clamped = Math.max(0, Math.min(1, p)); return Math.round(min + clamped * (max - min)); }, [max, min, value], ); useEffect(() => { const hit = hitRef.current; if (!hit) return; const onDown = (e: PointerEvent) => { e.preventDefault(); try { hit.setPointerCapture(e.pointerId); } catch { /* ignore */ } setDragging(true); onChange(valueFromClientX(e.clientX)); }; const onMove = (e: PointerEvent) => { if (!dragging) return; onChange(valueFromClientX(e.clientX)); }; const onUp = (e: PointerEvent) => { if (!dragging) return; setDragging(false); try { hit.releasePointerCapture(e.pointerId); } catch { /* ignore */ } }; hit.addEventListener('pointerdown', onDown); hit.addEventListener('pointermove', onMove); hit.addEventListener('pointerup', onUp); hit.addEventListener('pointercancel', onUp); return () => { hit.removeEventListener('pointerdown', onDown); hit.removeEventListener('pointermove', onMove); hit.removeEventListener('pointerup', onUp); hit.removeEventListener('pointercancel', onUp); }; }, [dragging, onChange, valueFromClientX]); const onFrameClick = (e: React.MouseEvent) => { if (dragging) return; if (e.target instanceof Element && e.target.closest('.pointer')) return; onChange(valueFromClientX(e.clientX)); }; const onKey = (e: React.KeyboardEvent) => { let next = value; if (e.key === 'ArrowRight' || e.key === 'ArrowUp') next = Math.min(max, value + (e.shiftKey ? 2 : 1)); else if (e.key === 'ArrowLeft' || e.key === 'ArrowDown') next = Math.max(min, value - (e.shiftKey ? 2 : 1)); else if (e.key === 'Home') next = min; else if (e.key === 'End') next = max; else return; e.preventDefault(); onChange(next); }; const ticks: number[] = []; for (let i = min; i <= max; i++) ticks.push(i); const mid = Math.round((min + max) / 2); return (
Устройства {value}  мест