Spaces:
Running
Running
| 'use client' | |
| import * as React from 'react' | |
| import * as SliderPrimitive from '@radix-ui/react-slider' | |
| import { ny } from '@/lib/utils' | |
| interface SliderProps extends React.ComponentPropsWithoutRef<typeof SliderPrimitive.Root> { | |
| showSteps?: 'none' | 'half' | 'full' | |
| formatLabel?: (value: number) => string | |
| formatLabelSide?: string | |
| } | |
| const Slider = React.forwardRef< | |
| React.ElementRef<typeof SliderPrimitive.Root>, | |
| SliderProps | |
| >(({ className, showSteps = 'none', formatLabel, formatLabelSide = 'top', ...props }, ref) => { | |
| const { min = 0, max = 100, step = 1, orientation = 'horizontal', value, defaultValue, onValueChange } = props | |
| const [hoveredThumbIndex, setHoveredThumbIndex] = React.useState<boolean>(false) | |
| const numberOfSteps = Math.floor((max - min) / step) | |
| const stepLines = Array.from({ length: numberOfSteps }, (_, index) => index * step + min) | |
| const initialValue = Array.isArray(value) ? value : (Array.isArray(defaultValue) ? defaultValue : [min, max]) | |
| const [localValues, setLocalValues] = React.useState<number[]>(initialValue) | |
| React.useEffect(() => { | |
| if (!isEqual(value, localValues)) | |
| setLocalValues(Array.isArray(value) ? value : (Array.isArray(defaultValue) ? defaultValue : [min, max])) | |
| }, [min, max, value]) | |
| const handleValueChange = (newValues: number[]) => { | |
| setLocalValues(newValues) | |
| if (onValueChange) | |
| onValueChange(newValues) | |
| } | |
| function isEqual(array1: number[] | undefined, array2: number[] | undefined) { | |
| array1 = array1 ?? [] | |
| array2 = array2 ?? [] | |
| if (array1.length !== array2.length) | |
| return false | |
| for (let i = 0; i < array1.length; i++) { | |
| if (array1[i] !== array2[i]) | |
| return false | |
| } | |
| return true | |
| } | |
| return ( | |
| <SliderPrimitive.Root | |
| ref={ref} | |
| className={ny( | |
| 'relative flex cursor-pointer touch-none select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50', | |
| orientation === 'horizontal' ? 'w-full items-center' : 'h-full justify-center', | |
| className, | |
| )} | |
| min={min} | |
| max={max} | |
| step={step} | |
| value={localValues} | |
| onValueChange={value => handleValueChange(value)} | |
| {...props} | |
| onFocus={() => setHoveredThumbIndex(true)} | |
| onBlur={() => setHoveredThumbIndex(false)} | |
| > | |
| <SliderPrimitive.Track className={ny( | |
| 'bg-primary/20 relative grow overflow-hidden rounded-full', | |
| orientation === 'horizontal' ? 'h-1.5 w-full' : 'h-full w-1.5', | |
| )} | |
| > | |
| <SliderPrimitive.Range className={ny( | |
| 'bg-primary absolute', | |
| orientation === 'horizontal' ? 'h-full' : 'w-full', | |
| )} | |
| /> | |
| {showSteps !== undefined && showSteps !== 'none' && stepLines.map((value, index) => { | |
| if (value === min || value === max) | |
| return null | |
| const positionPercentage = ((value - min) / (max - min)) * 100 | |
| const adjustedPosition = 50 + (positionPercentage - 50) * 0.96 | |
| return ( | |
| <div | |
| key={index} | |
| className={ny( | |
| { 'w-0.5 h-2': orientation !== 'vertical', 'w-2 h-0.5': orientation === 'vertical' }, | |
| 'bg-muted-foreground absolute', | |
| { | |
| 'left-1': orientation === 'vertical' && showSteps === 'half', | |
| 'top-1': orientation !== 'vertical' && showSteps === 'half', | |
| 'left-0': orientation === 'vertical' && showSteps === 'full', | |
| 'top-0': orientation !== 'vertical' && showSteps === 'full', | |
| '-translate-x-1/2': orientation !== 'vertical', | |
| '-translate-y-1/2': orientation === 'vertical', | |
| }, | |
| )} | |
| style={{ | |
| [orientation === 'vertical' ? 'bottom' : 'left']: `${adjustedPosition}%`, | |
| }} | |
| /> | |
| ) | |
| })} | |
| </SliderPrimitive.Track> | |
| {localValues.map((numberStep, index) => ( | |
| <SliderPrimitive.Thumb | |
| key={index} | |
| className={ny( | |
| 'border-primary/50 bg-background focus-visible:ring-ring block size-4 rounded-full border shadow transition-colors focus-visible:outline-none focus-visible:ring-1', | |
| )} | |
| > | |
| {hoveredThumbIndex && formatLabel && ( | |
| <div | |
| className={ny( | |
| { 'bottom-8 left-1/2 -translate-x-1/2': formatLabelSide === 'top' }, | |
| { 'top-8 left-1/2 -translate-x-1/2': formatLabelSide === 'bottom' }, | |
| { 'right-8 -translate-y-1/4': formatLabelSide === 'left' }, | |
| { 'left-8 -translate-y-1/4': formatLabelSide === 'right' }, | |
| 'bg-popover text-popover-foreground absolute z-30 w-max items-center justify-items-center rounded-md border px-2 py-1 text-center shadow-sm', | |
| )} | |
| > | |
| {formatLabel(numberStep)} | |
| </div> | |
| )} | |
| </SliderPrimitive.Thumb> | |
| ))} | |
| </SliderPrimitive.Root> | |
| ) | |
| }) | |
| Slider.displayName = SliderPrimitive.Root.displayName | |
| export { Slider } | |