// frontend\src\components\ui\Slider.tsx 'use client' import { useId, type SyntheticEvent, type CSSProperties } from 'react' type Props = { id?: string value: number onChange: (value: number) => void onChangeEnd?: (value: number) => void min?: number max?: number step?: number disabled?: boolean label?: string hint?: string valueLabel?: string valueFormatter?: (value: number) => string showValueLabel?: boolean showMinMaxLabels?: boolean minLabel?: string maxLabel?: string showReset?: boolean resetValue?: number resetLabel?: string compact?: boolean className?: string inputClassName?: string } function clamp(value: number, min: number, max: number) { return Math.max(min, Math.min(max, value)) } export default function Slider({ id, value, onChange, onChangeEnd, min = 0, max = 100, step = 1, disabled = false, label, hint, valueLabel, valueFormatter, showValueLabel = false, showMinMaxLabels = false, minLabel, maxLabel, showReset = false, resetValue, resetLabel = 'Reset', compact = false, className = '', inputClassName = '', }: Props) { const reactId = useId() const inputId = id ?? `slider-${reactId}` const safeValue = Number.isFinite(value) ? value : min const clamped = clamp(safeValue, min, max) const percent = max > min ? ((clamped - min) / (max - min)) * 100 : 0 const resolvedValueLabel = valueLabel ?? (valueFormatter ? valueFormatter(clamped) : String(clamped)) const resolvedMinLabel = minLabel ?? (valueFormatter ? valueFormatter(min) : String(min)) const resolvedMaxLabel = maxLabel ?? (valueFormatter ? valueFormatter(max) : String(max)) const canReset = showReset && typeof resetValue === 'number' && Number.isFinite(resetValue) const trackH = compact ? 4 : 8 const thumbSize = compact ? 14 : 16 const inputH = compact ? 14 : 16 const thumbOffset = -((thumbSize - trackH) / 2) const handleChange = (next: number) => { onChange(clamp(next, min, max)) } const handleCommit = (e: SyntheticEvent) => { onChangeEnd?.(clamp(Number(e.currentTarget.value), min, max)) } const handleReset = () => { if (!canReset) return const next = clamp(resetValue!, min, max) onChange(next) onChangeEnd?.(next) } return (
{compact ? ( <> {label ? ( ) : null}
handleChange(Number(e.target.value))} onPointerUp={handleCommit} onMouseUp={handleCommit} onTouchEnd={handleCommit} onKeyUp={(e) => { if ( e.key === 'ArrowLeft' || e.key === 'ArrowRight' || e.key === 'ArrowUp' || e.key === 'ArrowDown' || e.key === 'Home' || e.key === 'End' || e.key === 'PageUp' || e.key === 'PageDown' ) { onChangeEnd?.(clamp(Number(e.currentTarget.value), min, max)) } }} />
{showValueLabel ? (
{resolvedValueLabel}
) : null} {canReset ? ( ) : null} ) : ( <> {(label || hint || showValueLabel || canReset) ? (
{(label || hint) ? (
{label ? ( ) : null} {hint ? (
{hint}
) : null}
) : null} {(showValueLabel || canReset) ? (
{showValueLabel ? (
{resolvedValueLabel}
) : null} {canReset ? ( ) : null}
) : null}
) : null}
handleChange(Number(e.target.value))} onPointerUp={handleCommit} onMouseUp={handleCommit} onTouchEnd={handleCommit} onKeyUp={(e) => { if ( e.key === 'ArrowLeft' || e.key === 'ArrowRight' || e.key === 'ArrowUp' || e.key === 'ArrowDown' || e.key === 'Home' || e.key === 'End' || e.key === 'PageUp' || e.key === 'PageDown' ) { onChangeEnd?.(clamp(Number(e.currentTarget.value), min, max)) } }} />
{showMinMaxLabels ? (
{resolvedMinLabel} {resolvedMaxLabel}
) : null} )}
) }