import { type ComponentPropsWithoutRef, type CSSProperties } from 'react'; import { Slider as RSlider } from 'radix-ui'; type Mark = { value: number; label?: string }; type MarksProp = boolean | Array; type NotchPlacement = 'top' | 'bottom' | 'both' | 'none'; type KnobStyle = 'square' | 'round'; type SliderProps = Omit, 'className'> & { /** * Step marks. * - `true` — auto-generate one mark per `step` between `min` and `max`. * - array — explicit marks; numbers or `{ value, label }` for tick labels. */ marks?: MarksProp; /** * Where to draw the notch ticks relative to the track. * `'bottom'` (default), `'top'`, `'both'`, or `'none'` to hide ticks * (labels still render when provided). No effect without `marks`. */ notches?: NotchPlacement; /** Thumb shape. `'square'` (default) has a small border-radius; `'round'` is a full circle. */ knobStyle?: KnobStyle; /** * Enable step-glide animation. Defaults to `true` when `marks` is set, `false` otherwise. * Explicitly setting this always overrides the default. */ animated?: boolean; className?: string; }; function resolveMarks( marks: MarksProp, min: number, max: number, step: number, ): Mark[] { if (Array.isArray(marks)) { return marks .map((m) => (typeof m === 'number' ? { value: m } : m)) .filter((m) => m.value >= min && m.value <= max); } if (marks !== true) return []; if (!(step > 0) || max <= min) return []; const count = Math.floor((max - min) / step); // Guard against absurd notch counts (e.g. step=1 over a 0–1000 range). if (count < 1 || count > 100) return []; return Array.from({ length: count + 1 }, (_, i) => ({ value: min + i * step })); } const percent = (value: number, min: number, max: number) => max === min ? 0 : (value - min) / (max - min); const NotchLayer = ({ marks, min, max, side, }: { marks: Mark[]; min: number; max: number; side: 'top' | 'bottom'; }) => (
{marks.map((mark) => ( ))}
); export const Slider = ({ marks, notches = 'bottom', knobStyle = 'square', animated, min = 0, max = 100, step = 1, className, ...props }: SliderProps) => { const resolved = marks != null ? resolveMarks(marks, min, max, step) : []; const hasMarks = resolved.length > 0; const hasLabels = resolved.some((m) => m.label != null); const showTop = hasMarks && (notches === 'top' || notches === 'both'); const showBottom = hasMarks && (notches === 'bottom' || notches === 'both'); const isAnimated = animated !== undefined ? animated : hasMarks; const cls = [ 'modern-sk-slider', `modern-sk-slider--knob-${knobStyle}`, isAnimated && 'modern-sk-slider--animated', hasMarks && 'modern-sk-slider--has-marks', hasLabels && 'modern-sk-slider--has-labels', showTop && 'modern-sk-slider--notch-top', showBottom && 'modern-sk-slider--notch-bottom', className, ] .filter(Boolean) .join(' '); return ( {showTop && } {showBottom && } {hasLabels && (
{resolved.map((mark) => mark.label != null ? ( {mark.label} ) : null, )}
)}
); }; export const Stepper = ({ onDecrement, onIncrement, }: { onDecrement: () => void; onIncrement: () => void; }) => (
);