Spaces:
Running
Running
File size: 5,538 Bytes
e6ce630 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 |
'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 }
|