import { splitProps, mergeProps, createSignal, onCleanup, Show, JSX } from 'solid-js'; import clsx from 'clsx'; export interface SliderProps { value?: number; defaultValue?: number; min?: number; max?: number; step?: number; onChange?: (value: number) => void; size?: 'sm' | 'md' | 'lg'; disabled?: boolean; label?: string; showTooltip?: boolean; class?: string; } /** * Slider -- WAI-ARIA slider with glowing track, styled thumb, and value tooltip. * * Custom slider (not native input[type=range]) for angular clip-path geometry. * Supports controlled (value prop) and uncontrolled (defaultValue) modes. * Full keyboard support: Arrow keys, Home/End, PageUp/PageDown. */ export function Slider(rawProps: SliderProps) { const merged = mergeProps( { min: 0, max: 100, step: 1, size: 'md' as const, showTooltip: true, }, rawProps, ); const [local, rest] = splitProps(merged, [ 'value', 'defaultValue', 'min', 'max', 'step', 'onChange', 'size', 'disabled', 'label', 'showTooltip', 'class', ]); // Internal state for uncontrolled mode const [internalValue, setInternalValue] = createSignal(local.defaultValue ?? local.min); const [dragging, setDragging] = createSignal(false); const [hovering, setHovering] = createSignal(false); let trackRef: HTMLDivElement | undefined; // Resolve value: controlled vs uncontrolled const currentValue = () => { if (local.value !== undefined) { return local.value; } return internalValue(); }; const range = () => local.max - local.min; const percentage = () => ((currentValue() - local.min) / range()) * 100; const clampAndStep = (raw: number): number => { // Snap to step const stepped = Math.round((raw - local.min) / local.step) * local.step + local.min; // Clamp to range return Math.min(local.max, Math.max(local.min, stepped)); }; const updateValue = (newValue: number) => { const clamped = clampAndStep(newValue); if (local.value === undefined) { setInternalValue(clamped); } local.onChange?.(clamped); }; const getValueFromPosition = (clientX: number): number => { if (!trackRef) return currentValue(); const rect = trackRef.getBoundingClientRect(); const fraction = Math.max(0, Math.min(1, (clientX - rect.left) / rect.width)); return local.min + fraction * range(); }; // Mouse interaction handlers const handleMouseDown = (e: MouseEvent) => { if (local.disabled) return; e.preventDefault(); setDragging(true); updateValue(getValueFromPosition(e.clientX)); const handleMouseMove = (e: MouseEvent) => { updateValue(getValueFromPosition(e.clientX)); }; const handleMouseUp = () => { setDragging(false); document.removeEventListener('mousemove', handleMouseMove); document.removeEventListener('mouseup', handleMouseUp); }; document.addEventListener('mousemove', handleMouseMove); document.addEventListener('mouseup', handleMouseUp); }; // Keyboard interaction per WAI-ARIA slider pattern const handleKeyDown = (e: KeyboardEvent) => { if (local.disabled) return; let handled = true; const bigStep = local.step * 10; switch (e.key) { case 'ArrowRight': case 'ArrowUp': updateValue(currentValue() + local.step); break; case 'ArrowLeft': case 'ArrowDown': updateValue(currentValue() - local.step); break; case 'PageUp': updateValue(currentValue() + bigStep); break; case 'PageDown': updateValue(currentValue() - bigStep); break; case 'Home': updateValue(local.min); break; case 'End': updateValue(local.max); break; default: handled = false; } if (handled) { e.preventDefault(); } }; const showTooltipNow = () => local.showTooltip && (hovering() || dragging()); return (