@varialkit/slider 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/Slider.tsx ADDED
@@ -0,0 +1,574 @@
1
+ import React, { forwardRef, useCallback, useMemo, useRef, useState } from "react";
2
+ import { Tooltip } from "../../tooltip/src/Tooltip";
3
+ import type { SliderOrientation, SliderProps, SliderSize, SliderType, SliderValue } from "./Slider.types";
4
+ import "./Slider.scss";
5
+
6
+ type SliderThumb = "lower" | "upper";
7
+
8
+ const sizeAliasMap: Record<SliderSize, "small" | "medium" | "large" | "xlarge"> = {
9
+ sm: "small",
10
+ md: "medium",
11
+ lg: "large",
12
+ xl: "xlarge",
13
+ small: "small",
14
+ medium: "medium",
15
+ large: "large",
16
+ xlarge: "xlarge",
17
+ };
18
+
19
+ const clamp = (value: number, min: number, max: number) =>
20
+ Math.min(max, Math.max(min, value));
21
+
22
+ const roundToStep = (value: number, step: number, min: number) => {
23
+ if (!Number.isFinite(step) || step <= 0) return value;
24
+ const steps = Math.round((value - min) / step);
25
+ return min + steps * step;
26
+ };
27
+
28
+ const toNumber = (value: unknown, fallback: number) => {
29
+ const numeric = Number(value);
30
+ return Number.isFinite(numeric) ? numeric : fallback;
31
+ };
32
+
33
+ const isRangeValue = (value: SliderValue | undefined) => Array.isArray(value);
34
+
35
+ /**
36
+ * A slider component for selecting numeric values within a range.
37
+ */
38
+ export const Slider = forwardRef<HTMLInputElement, SliderProps>(
39
+ (
40
+ {
41
+ size = "medium",
42
+ type: sliderType = "knob",
43
+ showTooltip = false,
44
+ className,
45
+ trackClassName,
46
+ thumbClassName,
47
+ orientation = "horizontal",
48
+ min = 0,
49
+ max = 100,
50
+ value,
51
+ defaultValue,
52
+ onChange,
53
+ onValueChange,
54
+ disabled = false,
55
+ id,
56
+ step = 1,
57
+ range,
58
+ ...props
59
+ },
60
+ ref
61
+ ) => {
62
+ const resolvedSize = sizeAliasMap[size];
63
+ const inputId = id ?? React.useId();
64
+ const trackRef = useRef<HTMLDivElement>(null);
65
+ const lowerInputRef = useRef<HTMLInputElement>(null);
66
+ const upperInputRef = useRef<HTMLInputElement>(null);
67
+ const [isDragging, setIsDragging] = useState(false);
68
+ const [isHovered, setIsHovered] = useState(false);
69
+ const [focusedThumb, setFocusedThumb] = useState<SliderThumb | null>(null);
70
+ const [activeThumb, setActiveThumb] = useState<SliderThumb | null>(null);
71
+
72
+ const minValue = Number(min);
73
+ const maxValue = Number(max);
74
+
75
+ const isRange = range ?? (isRangeValue(value) || isRangeValue(defaultValue));
76
+ const isControlled = value !== undefined;
77
+
78
+ const [internalValue, setInternalValue] = useState<SliderValue>(() => {
79
+ if (value !== undefined) return value;
80
+ if (defaultValue !== undefined) return defaultValue;
81
+ return isRange ? [minValue, maxValue] : minValue;
82
+ });
83
+
84
+ React.useEffect(() => {
85
+ if (value === undefined) return;
86
+ setInternalValue(value);
87
+ }, [value]);
88
+
89
+ // Forward the ref to the lower (or single) input so external focus works.
90
+ const setLowerInput = useCallback(
91
+ (node: HTMLInputElement | null) => {
92
+ lowerInputRef.current = node;
93
+ if (!ref) return;
94
+ if (typeof ref === "function") {
95
+ ref(node);
96
+ } else {
97
+ ref.current = node;
98
+ }
99
+ },
100
+ [ref]
101
+ );
102
+
103
+ // Normalize any incoming range-like value into a safe, ordered tuple.
104
+ // This is the single place where we clamp to min/max, snap to step, and
105
+ // guarantee the lower bound never ends up above the upper bound.
106
+ const normalizeRange = useCallback(
107
+ (nextValue: SliderValue): [number, number] => {
108
+ const raw = Array.isArray(nextValue)
109
+ ? nextValue
110
+ : [toNumber(nextValue, minValue), toNumber(nextValue, maxValue)];
111
+ let lower = clamp(roundToStep(toNumber(raw[0], minValue), Number(step), minValue), minValue, maxValue);
112
+ let upper = clamp(roundToStep(toNumber(raw[1], maxValue), Number(step), minValue), minValue, maxValue);
113
+
114
+ if (lower > upper) {
115
+ [lower, upper] = [upper, lower];
116
+ }
117
+
118
+ return [lower, upper];
119
+ },
120
+ [maxValue, minValue, step]
121
+ );
122
+
123
+ const currentValue = isControlled ? value : internalValue;
124
+ const [lowerValue, upperValue] = isRange
125
+ ? normalizeRange(currentValue as SliderValue)
126
+ : [toNumber(currentValue, minValue), toNumber(currentValue, minValue)];
127
+
128
+ const rangeValue = maxValue - minValue;
129
+ const lowerProgress = rangeValue > 0 ? ((lowerValue - minValue) / rangeValue) * 100 : 0;
130
+ const upperProgress = rangeValue > 0 ? ((upperValue - minValue) / rangeValue) * 100 : 0;
131
+ const progress = rangeValue > 0 ? ((lowerValue - minValue) / rangeValue) * 100 : 0;
132
+
133
+ const isVertical = orientation === "vertical";
134
+
135
+ const sliderClasses = [
136
+ "solara-slider",
137
+ `solara-slider--size-${resolvedSize}`,
138
+ `solara-slider--${orientation}`,
139
+ `solara-slider--type-${sliderType}`,
140
+ isRange ? "solara-slider--range" : null,
141
+ disabled ? "solara-slider--disabled" : null,
142
+ isDragging ? "solara-slider--dragging" : null,
143
+ className,
144
+ ]
145
+ .filter(Boolean)
146
+ .join(" ");
147
+
148
+ const trackClasses = ["solara-slider__track", trackClassName]
149
+ .filter(Boolean)
150
+ .join(" ");
151
+ const thumbClasses = ["solara-slider__thumb", thumbClassName]
152
+ .filter(Boolean)
153
+ .join(" ");
154
+
155
+ // Pointer-driven track clicks/drags do not come from a native input change
156
+ // event, but consumers still expect `onChange` to receive the same shape.
157
+ // This synthetic event keeps single-value sliders backward compatible.
158
+ const createSyntheticChangeEvent = (next: number) =>
159
+ ({
160
+ target: { value: next.toString() },
161
+ currentTarget: { value: next.toString() },
162
+ }) as React.ChangeEvent<HTMLInputElement>;
163
+
164
+ // Single-value change funnel. All single-slider updates route through here so
165
+ // controlled and uncontrolled modes, `onValueChange`, and legacy `onChange`
166
+ // stay in sync no matter whether the value came from pointer, touch, or keys.
167
+ const emitSingleChange = useCallback(
168
+ (nextValue: number, event?: React.ChangeEvent<HTMLInputElement>) => {
169
+ if (!isControlled) {
170
+ setInternalValue(nextValue);
171
+ }
172
+ onValueChange?.(nextValue);
173
+ if (event) {
174
+ onChange?.(event);
175
+ } else {
176
+ onChange?.(createSyntheticChangeEvent(nextValue));
177
+ }
178
+ },
179
+ [isControlled, onChange, onValueChange]
180
+ );
181
+
182
+ // Range equivalent of `emitSingleChange`. We update the tuple for modern
183
+ // consumers via `onValueChange`, while `onChange` still receives the active
184
+ // thumb value so existing single-thumb handlers continue to behave sensibly.
185
+ const emitRangeChange = useCallback(
186
+ (
187
+ nextRange: [number, number],
188
+ thumbValue: number,
189
+ event?: React.ChangeEvent<HTMLInputElement>
190
+ ) => {
191
+ if (!isControlled) {
192
+ setInternalValue(nextRange);
193
+ }
194
+ onValueChange?.(nextRange);
195
+ if (event) {
196
+ onChange?.(event);
197
+ } else {
198
+ onChange?.(createSyntheticChangeEvent(thumbValue));
199
+ }
200
+ },
201
+ [isControlled, onChange, onValueChange]
202
+ );
203
+
204
+ // Convert the pointer location on the rendered track into a stepped slider
205
+ // value. Vertical sliders invert the axis so min stays at the bottom and max
206
+ // stays at the top.
207
+ const valueFromPointer = useCallback(
208
+ (clientX: number, clientY: number) => {
209
+ if (!trackRef.current) return null;
210
+ const track = trackRef.current.getBoundingClientRect();
211
+ const percentage = isVertical
212
+ ? clamp((track.bottom - clientY) / track.height, 0, 1)
213
+ : clamp((clientX - track.left) / track.width, 0, 1);
214
+ const rawValue = percentage * (maxValue - minValue) + minValue;
215
+ const steppedValue = roundToStep(rawValue, Number(step), minValue);
216
+ return clamp(steppedValue, minValue, maxValue);
217
+ },
218
+ [isVertical, maxValue, minValue, step]
219
+ );
220
+
221
+ // When clicking the track in range mode, move the closest thumb.
222
+ const pickClosestThumb = useCallback(
223
+ (nextValue: number) => {
224
+ const lowerDistance = Math.abs(nextValue - lowerValue);
225
+ const upperDistance = Math.abs(nextValue - upperValue);
226
+ if (lowerDistance === upperDistance) {
227
+ return nextValue < lowerValue ? "lower" : "upper";
228
+ }
229
+ return lowerDistance < upperDistance ? "lower" : "upper";
230
+ },
231
+ [lowerValue, upperValue]
232
+ );
233
+
234
+ // Update a single thumb while preventing it from crossing the other thumb.
235
+ const updateRangeValue = useCallback(
236
+ (nextValue: number, thumb: SliderThumb, event?: React.ChangeEvent<HTMLInputElement>) => {
237
+ if (disabled) return;
238
+ const nextLower = thumb === "lower" ? clamp(nextValue, minValue, upperValue) : lowerValue;
239
+ const nextUpper = thumb === "upper" ? clamp(nextValue, lowerValue, maxValue) : upperValue;
240
+ emitRangeChange([nextLower, nextUpper], nextValue, event);
241
+ },
242
+ [disabled, emitRangeChange, lowerValue, maxValue, minValue, upperValue]
243
+ );
244
+
245
+ // Shared pointer update path for track click, mouse drag, and touch drag.
246
+ // In range mode we resolve which thumb should move first, then keep focus on
247
+ // that thumb so keyboard interaction and tooltip visibility stay aligned.
248
+ const updateValueFromPointer = useCallback(
249
+ (clientX: number, clientY: number, thumbOverride?: SliderThumb) => {
250
+ const nextValue = valueFromPointer(clientX, clientY);
251
+ if (nextValue === null) return;
252
+
253
+ if (isRange) {
254
+ const thumb = thumbOverride ?? pickClosestThumb(nextValue);
255
+ updateRangeValue(nextValue, thumb);
256
+ setActiveThumb(thumb);
257
+ if (thumb === "lower") {
258
+ lowerInputRef.current?.focus();
259
+ } else {
260
+ upperInputRef.current?.focus();
261
+ }
262
+ } else {
263
+ emitSingleChange(nextValue);
264
+ lowerInputRef.current?.focus();
265
+ }
266
+ },
267
+ [emitSingleChange, isRange, pickClosestThumb, updateRangeValue, valueFromPointer]
268
+ );
269
+
270
+ const handleTrackClick = useCallback(
271
+ (event: React.MouseEvent<HTMLDivElement>) => {
272
+ event.preventDefault();
273
+ event.stopPropagation();
274
+ if (disabled) return;
275
+ updateValueFromPointer(event.clientX, event.clientY);
276
+ },
277
+ [disabled, updateValueFromPointer]
278
+ );
279
+
280
+ // Mouse dragging locks to whichever thumb was chosen on drag start so the
281
+ // active thumb does not switch back and forth mid-gesture.
282
+ const handleMouseDown = useCallback(
283
+ (event: React.MouseEvent) => {
284
+ if (disabled) return;
285
+ event.preventDefault();
286
+ setIsDragging(true);
287
+
288
+ const thumbOverride = isRange
289
+ ? pickClosestThumb(valueFromPointer(event.clientX, event.clientY) ?? lowerValue)
290
+ : undefined;
291
+ if (thumbOverride) {
292
+ setActiveThumb(thumbOverride);
293
+ }
294
+
295
+ const handleMouseMove = (moveEvent: MouseEvent) => {
296
+ updateValueFromPointer(moveEvent.clientX, moveEvent.clientY, thumbOverride ?? activeThumb ?? undefined);
297
+ };
298
+
299
+ const handleMouseUp = () => {
300
+ setIsDragging(false);
301
+ setActiveThumb(null);
302
+ document.removeEventListener("mousemove", handleMouseMove);
303
+ document.removeEventListener("mouseup", handleMouseUp);
304
+ };
305
+
306
+ document.addEventListener("mousemove", handleMouseMove);
307
+ document.addEventListener("mouseup", handleMouseUp);
308
+
309
+ updateValueFromPointer(event.clientX, event.clientY, thumbOverride ?? undefined);
310
+ },
311
+ [activeThumb, disabled, isRange, lowerValue, pickClosestThumb, updateValueFromPointer, valueFromPointer]
312
+ );
313
+
314
+ // Touch uses the same thumb-locking behavior as mouse drag, but keeps the
315
+ // listeners on `document` so the gesture continues even if the finger moves
316
+ // outside the track bounds.
317
+ const handleTouchStart = useCallback(
318
+ (event: React.TouchEvent) => {
319
+ if (disabled) return;
320
+ event.preventDefault();
321
+ setIsDragging(true);
322
+
323
+ const initialTouch = event.touches[0];
324
+ const thumbOverride =
325
+ isRange && initialTouch
326
+ ? pickClosestThumb(valueFromPointer(initialTouch.clientX, initialTouch.clientY) ?? lowerValue)
327
+ : undefined;
328
+ if (thumbOverride) {
329
+ setActiveThumb(thumbOverride);
330
+ }
331
+
332
+ const handleTouchMove = (moveEvent: TouchEvent) => {
333
+ if (moveEvent.touches.length === 0) return;
334
+ updateValueFromPointer(
335
+ moveEvent.touches[0].clientX,
336
+ moveEvent.touches[0].clientY,
337
+ thumbOverride ?? activeThumb ?? undefined
338
+ );
339
+ };
340
+
341
+ const handleTouchEnd = () => {
342
+ setIsDragging(false);
343
+ setActiveThumb(null);
344
+ document.removeEventListener("touchmove", handleTouchMove);
345
+ document.removeEventListener("touchend", handleTouchEnd);
346
+ };
347
+
348
+ document.addEventListener("touchmove", handleTouchMove, { passive: false });
349
+ document.addEventListener("touchend", handleTouchEnd);
350
+
351
+ if (initialTouch) {
352
+ updateValueFromPointer(initialTouch.clientX, initialTouch.clientY, thumbOverride ?? undefined);
353
+ }
354
+ },
355
+ [activeThumb, disabled, isRange, lowerValue, pickClosestThumb, updateValueFromPointer, valueFromPointer]
356
+ );
357
+
358
+ // Keyboard controls: arrow keys, Home/End.
359
+ const handleKeyDown = useCallback(
360
+ (event: React.KeyboardEvent<HTMLInputElement>, thumb: SliderThumb) => {
361
+ if (disabled) return;
362
+
363
+ const stepValue = Number(step) || 1;
364
+ const currentThumbValue = thumb === "lower" ? lowerValue : upperValue;
365
+ let nextValue = currentThumbValue;
366
+
367
+ switch (event.key) {
368
+ case "ArrowLeft":
369
+ case "ArrowDown":
370
+ event.preventDefault();
371
+ nextValue = currentThumbValue - stepValue;
372
+ break;
373
+ case "ArrowRight":
374
+ case "ArrowUp":
375
+ event.preventDefault();
376
+ nextValue = currentThumbValue + stepValue;
377
+ break;
378
+ case "Home":
379
+ event.preventDefault();
380
+ nextValue = minValue;
381
+ break;
382
+ case "End":
383
+ event.preventDefault();
384
+ nextValue = maxValue;
385
+ break;
386
+ default:
387
+ return;
388
+ }
389
+
390
+ const clamped = clamp(nextValue, minValue, maxValue);
391
+ if (isRange) {
392
+ updateRangeValue(clamped, thumb);
393
+ } else {
394
+ emitSingleChange(clamped);
395
+ }
396
+ },
397
+ [disabled, emitSingleChange, isRange, lowerValue, maxValue, minValue, step, updateRangeValue, upperValue]
398
+ );
399
+
400
+ // Native range inputs still drive keyboard interaction and browser-level
401
+ // accessibility. This handler normalizes those updates back into the same
402
+ // single/range change funnels used by pointer interaction.
403
+ const handleInputChange = useCallback(
404
+ (event: React.ChangeEvent<HTMLInputElement>, thumb: SliderThumb) => {
405
+ const nextValue = Number(event.target.value);
406
+ if (isRange) {
407
+ updateRangeValue(nextValue, thumb, event);
408
+ } else {
409
+ emitSingleChange(nextValue, event);
410
+ }
411
+ },
412
+ [emitSingleChange, isRange, updateRangeValue]
413
+ );
414
+
415
+ // Track fill uses start/end so range mode paints only the selected span.
416
+ const trackStyle = useMemo(
417
+ () =>
418
+ ({
419
+ // Start/end are used by the track fill to render the active span.
420
+ "--slider-range-start": `${clamp(isRange ? lowerProgress : 0, 0, 100)}%`,
421
+ "--slider-range-end": `${clamp(isRange ? upperProgress : progress, 0, 100)}%`,
422
+ }) as React.CSSProperties,
423
+ [isRange, lowerProgress, progress, upperProgress]
424
+ );
425
+
426
+ // Thumb and tooltip-trigger positions are driven by CSS custom properties.
427
+ // We expose both percentage and decimal progress values so the default knob
428
+ // variant can position from a simple percentage while the thumb variant can
429
+ // do pixel-accurate math against the track's usable size (track minus padding).
430
+ const lowerThumbStyle = useMemo(
431
+ () =>
432
+ ({
433
+ // Percent-based progress for the default knob thumb.
434
+ "--slider-progress": `${clamp(isRange ? lowerProgress : progress, 0, 100)}%`,
435
+ // Decimal progress lets thumb variants compute pixel-accurate positions.
436
+ "--slider-progress-decimal": `${clamp(isRange ? lowerProgress : progress, 0, 100) / 100}`,
437
+ }) as React.CSSProperties,
438
+ [isRange, lowerProgress, progress]
439
+ );
440
+
441
+ const upperThumbStyle = useMemo(
442
+ () =>
443
+ ({
444
+ // Percent-based progress for the upper thumb in range mode.
445
+ "--slider-progress": `${clamp(upperProgress, 0, 100)}%`,
446
+ // Decimal progress mirrors the lower thumb for variant math.
447
+ "--slider-progress-decimal": `${clamp(upperProgress, 0, 100) / 100}`,
448
+ }) as React.CSSProperties,
449
+ [upperProgress]
450
+ );
451
+
452
+ // Tooltip visibility is derived from slider interaction state rather than the
453
+ // Tooltip component's built-in hover/focus handlers. This keeps value bubbles
454
+ // visible while dragging and ensures range sliders only show the active thumb.
455
+ const showLowerTooltip =
456
+ showTooltip &&
457
+ (isHovered || focusedThumb === "lower" || (isDragging && (activeThumb === "lower" || !isRange)));
458
+ const showUpperTooltip =
459
+ showTooltip && (isHovered || focusedThumb === "upper" || (isDragging && activeThumb === "upper"));
460
+
461
+ return (
462
+ <div
463
+ className={sliderClasses}
464
+ onMouseEnter={() => setIsHovered(true)}
465
+ onMouseLeave={() => setIsHovered(false)}>
466
+ <div className="solara-slider__container">
467
+ <div
468
+ ref={trackRef}
469
+ className="solara-slider__track-wrapper"
470
+ onClick={handleTrackClick}
471
+ onMouseDown={handleMouseDown}
472
+ onTouchStart={handleTouchStart}>
473
+ <div className={trackClasses} style={trackStyle}>
474
+ <div className="solara-slider__range" />
475
+ </div>
476
+ </div>
477
+
478
+ {isRange ? (
479
+ <>
480
+ <input
481
+ id={`${inputId}-lower`}
482
+ ref={setLowerInput}
483
+ type="range"
484
+ className="solara-slider__input solara-slider__input--range"
485
+ min={min}
486
+ max={upperValue}
487
+ step={step}
488
+ value={lowerValue}
489
+ disabled={disabled}
490
+ onFocus={() => setFocusedThumb("lower")}
491
+ onBlur={() => setFocusedThumb(null)}
492
+ onKeyDown={(event) => handleKeyDown(event, "lower")}
493
+ onChange={(event) => handleInputChange(event, "lower")}
494
+ tabIndex={disabled ? -1 : 0}
495
+ aria-valuemin={Number(min)}
496
+ aria-valuemax={Number(upperValue)}
497
+ aria-valuenow={Number(lowerValue)}
498
+ aria-orientation={orientation}
499
+ {...props}
500
+ />
501
+ <input
502
+ id={`${inputId}-upper`}
503
+ ref={upperInputRef}
504
+ type="range"
505
+ className="solara-slider__input solara-slider__input--range"
506
+ min={lowerValue}
507
+ max={max}
508
+ step={step}
509
+ value={upperValue}
510
+ disabled={disabled}
511
+ onFocus={() => setFocusedThumb("upper")}
512
+ onBlur={() => setFocusedThumb(null)}
513
+ onKeyDown={(event) => handleKeyDown(event, "upper")}
514
+ onChange={(event) => handleInputChange(event, "upper")}
515
+ tabIndex={disabled ? -1 : 0}
516
+ aria-valuemin={Number(lowerValue)}
517
+ aria-valuemax={Number(max)}
518
+ aria-valuenow={Number(upperValue)}
519
+ aria-orientation={orientation}
520
+ {...props}
521
+ />
522
+ </>
523
+ ) : (
524
+ <input
525
+ id={inputId}
526
+ ref={setLowerInput}
527
+ type="range"
528
+ className="solara-slider__input"
529
+ min={min}
530
+ max={max}
531
+ step={step}
532
+ value={lowerValue}
533
+ disabled={disabled}
534
+ onFocus={() => setFocusedThumb("lower")}
535
+ onBlur={() => setFocusedThumb(null)}
536
+ onKeyDown={(event) => handleKeyDown(event, "lower")}
537
+ onChange={(event) => handleInputChange(event, "lower")}
538
+ tabIndex={disabled ? -1 : 0}
539
+ aria-valuemin={Number(min)}
540
+ aria-valuemax={Number(max)}
541
+ aria-valuenow={Number(lowerValue)}
542
+ aria-orientation={orientation}
543
+ {...props}
544
+ />
545
+ )}
546
+
547
+ {/*
548
+ Tooltip is anchored to a dedicated 1px trigger element that receives the
549
+ same CSS position variables as the visible thumb. The visible thumb stays
550
+ separate so Tooltip's internal trigger wrapper cannot disturb thumb layout.
551
+ */}
552
+ <span className="solara-slider__tooltip-trigger" style={lowerThumbStyle}>
553
+ <Tooltip content={lowerValue} position={isVertical ? "right" : "top"} hideArrow open={showLowerTooltip} offset={isVertical ? 14 : 22}>
554
+ <span className="solara-slider__tooltip-anchor solara-slider__tooltip-anchor--lower" />
555
+ </Tooltip>
556
+ </span>
557
+ <div className={`${thumbClasses} solara-slider__thumb--lower`} style={lowerThumbStyle} />
558
+ {isRange ? (
559
+ <>
560
+ <span className="solara-slider__tooltip-trigger" style={upperThumbStyle}>
561
+ <Tooltip content={upperValue} position={isVertical ? "right" : "top"} hideArrow open={showUpperTooltip} offset={isVertical ? 14 : 22}>
562
+ <span className="solara-slider__tooltip-anchor solara-slider__tooltip-anchor--upper" />
563
+ </Tooltip>
564
+ </span>
565
+ <div className={`${thumbClasses} solara-slider__thumb--upper`} style={upperThumbStyle} />
566
+ </>
567
+ ) : null}
568
+ </div>
569
+ </div>
570
+ );
571
+ }
572
+ );
573
+
574
+ Slider.displayName = "Slider";
@@ -0,0 +1,62 @@
1
+ import React from "react";
2
+
3
+ export type SliderSize = "small" | "medium" | "large" | "xlarge" | "sm" | "md" | "lg" | "xl";
4
+ export type SliderValue = number | [number, number];
5
+ export type SliderOrientation = "horizontal" | "vertical";
6
+ export type SliderType = "knob" | "thumb";
7
+
8
+ export interface SliderProps
9
+ extends Omit<
10
+ React.InputHTMLAttributes<HTMLInputElement>,
11
+ "size" | "onChange" | "value" | "defaultValue" | "type"
12
+ > {
13
+ /**
14
+ * Visual style of the slider.
15
+ */
16
+ type?: SliderType;
17
+ /**
18
+ * Size of the slider.
19
+ */
20
+ size?: SliderSize;
21
+ /**
22
+ * Orientation of the slider.
23
+ */
24
+ orientation?: SliderOrientation;
25
+ /**
26
+ * Enable range selection (two thumbs).
27
+ * When omitted, range mode is inferred from `value` or `defaultValue`.
28
+ */
29
+ range?: boolean;
30
+ /**
31
+ * Controlled value for the slider (number for single, tuple for range).
32
+ */
33
+ value?: SliderValue;
34
+ /**
35
+ * Uncontrolled default value (number for single, tuple for range).
36
+ */
37
+ defaultValue?: SliderValue;
38
+ /**
39
+ * Show tooltip with the current value.
40
+ */
41
+ showTooltip?: boolean;
42
+ /**
43
+ * Custom class name for the slider container.
44
+ */
45
+ className?: string;
46
+ /**
47
+ * Custom class name for the track element.
48
+ */
49
+ trackClassName?: string;
50
+ /**
51
+ * Custom class name for the thumb element.
52
+ */
53
+ thumbClassName?: string;
54
+ /**
55
+ * Callback when value changes.
56
+ */
57
+ onChange?: (event: React.ChangeEvent<HTMLInputElement>) => void;
58
+ /**
59
+ * Callback when value changes (number for single, tuple for range).
60
+ */
61
+ onValueChange?: (value: SliderValue) => void;
62
+ }
package/src/index.ts ADDED
@@ -0,0 +1,2 @@
1
+ export { Slider } from "./Slider";
2
+ export type { SliderProps, SliderSize, SliderValue, SliderOrientation, SliderType } from "./Slider.types";