refactor: Update CreateThemePage component to dynamically generate theme data
This commit is contained in:
136
src/components/ui/slider.tsx
Normal file
136
src/components/ui/slider.tsx
Normal file
@@ -0,0 +1,136 @@
|
||||
'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 }
|
||||
Reference in New Issue
Block a user