/** * @title Range * @description 최솟값과 최댓값 사이에서 대략적인 값을 슬라이더로 고르게 해요. */import * as React from 'react';import { Slider as SliderPrimitive } from 'radix-ui';import { cn } from '@/lib/utils';export interface RangeProps extends Omit< React.ComponentProps<typeof SliderPrimitive.Root>, 'value' | 'defaultValue' | 'onChange' | 'onValueChange' | 'disabled' | 'min' | 'max' | 'step' | 'orientation' > { /** 비제어 초기값 */ defaultValue?: number; /** 제어 값 */ value?: number; /** 값이 바뀔 때 숫자 하나로 호출돼요. */ onChange?: (value: number) => void; isDisabled?: boolean; min?: number; max?: number; step?: number; /** 썸(slider 역할)의 id. 라벨 htmlFor 와 연결해요. */ id?: string; 'aria-label'?: string; 'aria-labelledby'?: string; 'aria-describedby'?: string; /** 스크린 리더가 읽을 값 문구 (예: "50%") */ 'aria-valuetext'?: string; testId?: string;}// @atlaskit/range styled.compiled 스펙// 높이 40px, 트랙 6px(radius full), 썸 16px, 끝 점 4pxfunction Range({ defaultValue = 50, value, onChange, isDisabled = false, min = 0, max = 100, step = 1, id, name, 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy, 'aria-describedby': ariaDescribedBy, 'aria-valuetext': ariaValueText, testId, className, ...props}: RangeProps) { const [inner, setInner] = React.useState(defaultValue); const current = value ?? inner; return ( <SliderPrimitive.Root data-slot="range" data-testid={testId} min={min} max={max} step={step} name={name} disabled={isDisabled} value={[current]} onValueChange={([next]) => { if (value === undefined) setInner(next); onChange?.(next); }} className={cn( 'group/range relative flex h-10 w-full touch-none items-center select-none', 'data-disabled:cursor-not-allowed data-disabled:opacity-(--ds-opacity-disabled)', // 트랙 끝 점 (최댓값 위치 표시) "after:pointer-events-none after:absolute after:start-[calc(100%-3px)] after:top-1/2 after:block after:size-1 after:-translate-y-1/2 after:rounded-ds-full after:bg-ds-background-neutral-bold-pressed after:content-['']", className, )} {...props} > <SliderPrimitive.Track data-slot="range-track" className={cn( 'relative h-1.5 w-full grow cursor-pointer overflow-hidden rounded-ds-full bg-ds-background-inverse-subtle', 'transition-colors duration-200 ease-in-out', 'group-hover/range:group-not-data-disabled/range:bg-ds-background-inverse-subtle-hovered', 'group-data-disabled/range:cursor-not-allowed', )} > <SliderPrimitive.Range data-slot="range-progress" className={cn( 'absolute h-full rounded-ds-full bg-ds-background-neutral-bold-pressed transition-colors duration-200 ease-in-out', 'group-hover/range:group-not-data-disabled/range:bg-ds-background-neutral-bold-hovered', )} /> </SliderPrimitive.Track> <SliderPrimitive.Thumb id={id} data-slot="range-thumb" aria-label={ariaLabel} aria-labelledby={ariaLabelledBy} aria-describedby={ariaDescribedBy} aria-valuetext={ariaValueText} className={cn( 'relative z-1 block size-4 cursor-pointer rounded-ds-full border-none bg-ds-background-neutral-bold-pressed', 'transition-colors duration-200 ease-in-out', 'outline-2 outline-offset-2 outline-transparent outline-solid focus-visible:outline-ds-border-focused', 'group-hover/range:group-not-data-disabled/range:bg-ds-background-neutral-bold-hovered', 'active:group-not-data-disabled/range:bg-ds-background-neutral-bold-pressed', 'group-data-disabled/range:cursor-not-allowed', )} /> </SliderPrimitive.Root> );}export { Range };