fix: SliderControl is now

This commit is contained in:
Alan Trebugeais
2026-05-10 00:19:03 +02:00
parent f6a40d7324
commit fd9bd1a755
+63 -20
View File
@@ -1,5 +1,5 @@
import type { PointerEvent as ReactPointerEvent } from "react";
import { useCallback, useRef } from "react";
import { useCallback, useRef, memo, useEffect } from "react";
import { cn } from "@/lib/utils";
interface SliderControlProps {
@@ -27,7 +27,7 @@ function quantizeToStep(value: number, min: number, step: number) {
return min + Math.round((value - min) / step) * step;
}
export function SliderControl({
export const SliderControl = memo(function SliderControl({
label,
value,
defaultValue: _defaultValue,
@@ -40,30 +40,49 @@ export function SliderControl({
accentColor = "blue",
}: SliderControlProps) {
const rootRef = useRef<HTMLDivElement | null>(null);
const valueTextRef = useRef<HTMLSpanElement | null>(null);
const boundsRef = useRef<DOMRect | null>(null);
const requestRef = useRef<number | null>(null);
const pct = Math.min(100, Math.max(0, ((value - min) / (max - min || 1)) * 100));
const dividerClass =
accentColor === "purple"
? "bg-foreground/95 shadow-[0_0_10px_rgba(139,92,246,0.28)]"
: "bg-foreground/95 shadow-[0_0_10px_rgba(37,99,235,0.28)]";
const setValueFromClientX = useCallback(
// Sync initial and prop-driven changes to CSS variable
useEffect(() => {
if (rootRef.current) {
rootRef.current.style.setProperty("--slider-pct", `${pct}%`);
}
}, [pct]);
const updateValue = useCallback(
(clientX: number) => {
const root = rootRef.current;
if (!root) {
const bounds = boundsRef.current;
if (!bounds || bounds.width <= 6) {
return;
}
const bounds = root.getBoundingClientRect();
if (!(bounds.width > 0)) {
return;
}
const normalized = clamp((clientX - bounds.left) / bounds.width, 0, 1);
const normalized = clamp((clientX - (bounds.left + 3)) / (bounds.width - 6), 0, 1);
const rawValue = min + normalized * (max - min);
const nextValue = clamp(quantizeToStep(rawValue, min, step), min, max);
onChange(Number(nextValue.toFixed(6)));
const finalValue = Number(nextValue.toFixed(6));
const currentPct = (normalized * 100).toFixed(4);
// Direct DOM update for instant feedback
if (rootRef.current) {
rootRef.current.style.setProperty("--slider-pct", `${currentPct}%`);
}
if (valueTextRef.current) {
valueTextRef.current.textContent = formatValue(finalValue);
}
// Notify parent
onChange(finalValue);
},
[max, min, onChange, step],
[max, min, onChange, step, formatValue],
);
const handlePointerDown = useCallback(
@@ -72,15 +91,24 @@ export function SliderControl({
const pointerId = event.pointerId;
const target = event.currentTarget;
// Cache bounds to avoid layout thrashing during move
boundsRef.current = target.getBoundingClientRect();
target.setPointerCapture(pointerId);
setValueFromClientX(event.clientX);
updateValue(event.clientX);
const handlePointerMove = (moveEvent: PointerEvent) => {
if (moveEvent.pointerId !== pointerId) {
return;
}
setValueFromClientX(moveEvent.clientX);
if (requestRef.current) {
cancelAnimationFrame(requestRef.current);
}
requestRef.current = requestAnimationFrame(() => {
updateValue(moveEvent.clientX);
});
};
const finishPointer = (finishEvent: PointerEvent) => {
@@ -88,17 +116,22 @@ export function SliderControl({
return;
}
if (requestRef.current) {
cancelAnimationFrame(requestRef.current);
}
target.releasePointerCapture(pointerId);
target.removeEventListener("pointermove", handlePointerMove);
target.removeEventListener("pointerup", finishPointer);
target.removeEventListener("pointercancel", finishPointer);
boundsRef.current = null;
};
target.addEventListener("pointermove", handlePointerMove);
target.addEventListener("pointerup", finishPointer);
target.addEventListener("pointercancel", finishPointer);
},
[setValueFromClientX],
[updateValue],
);
return (
@@ -124,11 +157,16 @@ export function SliderControl({
}
}}
className="relative flex h-10 w-full select-none items-center overflow-hidden rounded-xl bg-editor-bg/80 px-1.5 outline-none focus-visible:ring-1 focus-visible:ring-[#2563EB]/40"
style={
{
"--slider-pct": `${pct}%`,
} as React.CSSProperties
}
>
<div
className="pointer-events-none absolute inset-y-[3px] left-[3px] right-auto rounded-[10px] bg-foreground/[0.08] shadow-[0_4px_10px_0_rgba(0,0,0,0.18)] transition-none"
style={{
width: pct > 0 ? `max(calc(${pct}% - 6px), 2.1rem)` : 0,
width: "calc(var(--slider-pct) - (var(--slider-pct) * 6px / 100%))",
}}
/>
<div
@@ -136,14 +174,19 @@ export function SliderControl({
"pointer-events-none absolute bottom-[18%] top-[18%] z-10 w-[2px] rounded-full transition-none",
dividerClass,
)}
style={{ left: `calc(${pct}% - 8px)` }}
style={{
left: "calc(2px + var(--slider-pct) - (var(--slider-pct) * 6px / 100%))",
}}
/>
<span className="pointer-events-none relative z-10 flex-1 pl-3 text-[12px] font-medium text-muted-foreground">
{label}
</span>
<span className="pointer-events-none relative z-10 pr-3 text-[12px] font-medium tabular-nums text-foreground">
<span
ref={valueTextRef}
className="pointer-events-none relative z-10 pr-3 text-[12px] font-medium tabular-nums text-foreground"
>
{formatValue(value)}
</span>
</div>
);
}
});