mirror of
https://github.com/CompassConnections/Compass.git
synced 2026-02-20 07:44:01 -05:00
182 lines
4.6 KiB
TypeScript
182 lines
4.6 KiB
TypeScript
import clsx from 'clsx'
|
|
import * as RxSlider from '@radix-ui/react-slider'
|
|
import {ReactNode, useEffect, useState} from 'react'
|
|
|
|
const colors = {
|
|
green: ['bg-teal-400', 'focus:outline-teal-600/30 bg-teal-600'],
|
|
'light-green': [
|
|
'bg-emerald-200/50 dark:bg-emerald-800/50',
|
|
'focus:outline-emerald-200/20 bg-emerald-200 dark:bg-teal-200',
|
|
],
|
|
red: ['bg-scarlet-400', 'focus:outline-scarlet-600/30 bg-scarlet-600'],
|
|
indigo: ['bg-primary-300', 'focus:outline-primary-500/30 bg-primary-500'],
|
|
// light: ['primary-200', 'primary-300']
|
|
} as const
|
|
export type Mark = { value: number; label: string }
|
|
|
|
export function Slider(props: {
|
|
amount: number
|
|
onChange: (newAmount: number) => void
|
|
min?: number
|
|
max?: number
|
|
step?: number
|
|
marks?: Mark[]
|
|
color?: keyof typeof colors
|
|
className?: string
|
|
disabled?: boolean
|
|
inverted?: boolean
|
|
}) {
|
|
const {
|
|
amount,
|
|
onChange,
|
|
min = 0,
|
|
max = 100,
|
|
step,
|
|
marks,
|
|
className,
|
|
disabled,
|
|
color = 'indigo',
|
|
inverted,
|
|
} = props
|
|
|
|
const [trackClasses, thumbClasses] = colors[color]
|
|
|
|
return (
|
|
<RxSlider.Root
|
|
className={clsx(
|
|
className,
|
|
'relative flex touch-none select-none items-center',
|
|
marks ? 'h-[42px]' : 'h-5'
|
|
)}
|
|
value={[amount]}
|
|
onValueChange={([val]) => onChange(val)}
|
|
min={min}
|
|
max={max}
|
|
step={step}
|
|
disabled={disabled}
|
|
inverted={inverted}
|
|
>
|
|
<Track className={trackClasses}>
|
|
<div className="absolute left-2.5 right-2.5 h-full">
|
|
{marks?.map(({value, label}) => (
|
|
<div
|
|
className="absolute top-1/2 -translate-x-1/2 -translate-y-1/2"
|
|
style={{left: `${(value / (max - min)) * 100}%`}}
|
|
key={value}
|
|
>
|
|
<div
|
|
className={clsx(
|
|
amount >= value ? trackClasses : 'bg-ink-400',
|
|
'h-2 w-2 rounded-full'
|
|
)}
|
|
/>
|
|
<span className="text-ink-400 absolute left-1/2 top-4 -translate-x-1/2 text-xs">
|
|
{label}
|
|
</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</Track>
|
|
<Thumb className={thumbClasses}/>
|
|
</RxSlider.Root>
|
|
)
|
|
}
|
|
|
|
export function RangeSlider(props: {
|
|
lowValue: number
|
|
highValue: number
|
|
setValues: (low: number, high: number) => void
|
|
min?: number
|
|
max?: number
|
|
disabled?: boolean
|
|
step?: number
|
|
color?: keyof typeof colors
|
|
handleSize?: number
|
|
className?: string
|
|
marks?: Mark[]
|
|
}) {
|
|
const {
|
|
lowValue,
|
|
highValue,
|
|
setValues,
|
|
min,
|
|
max,
|
|
step,
|
|
disabled,
|
|
color = 'indigo',
|
|
className,
|
|
marks,
|
|
} = props
|
|
|
|
const [trackClasses, thumbClasses] = colors[color]
|
|
const [dragValues, setDragValues] = useState<[number, number]>([lowValue, highValue])
|
|
|
|
// keep local drag state in sync with external values
|
|
useEffect(() => {
|
|
setDragValues([lowValue, highValue])
|
|
}, [lowValue, highValue])
|
|
|
|
// debounce parent updates while dragging to avoid excessive re-renders/queries
|
|
useEffect(() => {
|
|
const [low, high] = dragValues
|
|
const t = setTimeout(() => {
|
|
setValues(low, high)
|
|
}, 200)
|
|
return () => clearTimeout(t)
|
|
}, [dragValues])
|
|
|
|
return (
|
|
<RxSlider.Root
|
|
className={clsx(
|
|
className,
|
|
'relative flex h-7 touch-none select-none items-center'
|
|
)}
|
|
value={dragValues}
|
|
step={step ?? 1}
|
|
onValueChange={(vals: number[]) => setDragValues([vals[0], vals[1]])} // update continuously for UI feedback
|
|
min={min}
|
|
max={max}
|
|
disabled={disabled}
|
|
>
|
|
<Track className={trackClasses}>
|
|
<div>
|
|
{marks?.map(({value, label}) => (
|
|
<div
|
|
className="absolute top-1/2 -translate-x-1/2 -translate-y-1/2"
|
|
style={{left: `${value}%`}}
|
|
key={value}
|
|
>
|
|
<span className="text-ink-400 absolute left-1/2 top-4 -translate-x-1/2 text-xs">
|
|
{label}
|
|
</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</Track>
|
|
<Thumb className={thumbClasses}/>
|
|
<Thumb className={thumbClasses}/>
|
|
</RxSlider.Root>
|
|
)
|
|
}
|
|
|
|
const Track = (props: { className: string; children?: ReactNode }) => {
|
|
const {className, children} = props
|
|
return (
|
|
<RxSlider.Track className="bg-ink-300 relative h-1 grow rounded-full">
|
|
{children}
|
|
<RxSlider.Range
|
|
className={clsx(className, 'absolute h-full rounded-full')}
|
|
/>
|
|
</RxSlider.Track>
|
|
)
|
|
}
|
|
|
|
const Thumb = (props: { className: string }) => (
|
|
<RxSlider.Thumb
|
|
className={clsx(
|
|
props.className,
|
|
'block h-5 w-5 cursor-col-resize rounded-full outline outline-4 outline-transparent transition-colors'
|
|
)}
|
|
/>
|
|
)
|