(null);
+ const dragRef = useRef<{ pointerId: number; grabOffsetPx: number } | null>(null);
+ const valueRef = useRef(value);
+ valueRef.current = value;
+
+ const finishDrag = (pointerId: number | null) => {
+ const drag = dragRef.current;
+ if (!drag) return;
+ if (pointerId !== null && drag.pointerId !== pointerId) return;
+ dragRef.current = null;
+ onRelease?.(valueRef.current);
+ onDragEnd?.();
+ };
+
+ return (
+ {
+ if (disabled || dragRef.current) return;
+ if (e.pointerType === "mouse" && e.button !== 0) return;
+ const input = inputRef.current;
+ if (!input) return;
+ const rect = input.getBoundingClientRect();
+ const pressX = e.clientX - rect.left;
+ const centerX = thumbCenterXPx(rect.width, min, max, valueRef.current);
+ // Track press: ignore entirely — no value change, no drag.
+ if (Math.abs(pressX - centerX) > THUMB_HIT_RADIUS_PX) return;
+ e.preventDefault();
+ dragRef.current = { pointerId: e.pointerId, grabOffsetPx: pressX - centerX };
+ onDragStart?.();
+ e.currentTarget.setPointerCapture(e.pointerId);
+ }}
+ onPointerMove={(e) => {
+ const drag = dragRef.current;
+ if (disabled || !drag || drag.pointerId !== e.pointerId) return;
+ const input = inputRef.current;
+ if (!input) return;
+ const rect = input.getBoundingClientRect();
+ const offsetX = e.clientX - rect.left - drag.grabOffsetPx;
+ onChange(valueFromOffsetXPx(rect.width, min, max, offsetX));
+ }}
+ onPointerUp={(e) => {
+ const drag = dragRef.current;
+ if (!drag || drag.pointerId !== e.pointerId) return;
+ try {
+ e.currentTarget.releasePointerCapture(e.pointerId);
+ } catch {
+ // ignore
+ }
+ finishDrag(e.pointerId);
+ }}
+ onPointerCancel={() => finishDrag(null)}
+ onLostPointerCapture={() => finishDrag(null)}
+ >
+ {
+ // Only reachable via keyboard — the input no longer receives pointer input.
+ if (disabled || dragRef.current) return;
+ onChange(Number(e.target.value));
+ }}
+ onBlur={(e) => {
+ if (dragRef.current) {
+ finishDrag(null);
+ return;
+ }
+ if (!disabled) {
+ onRelease?.(Number(e.currentTarget.value));
+ }
+ }}
+ onKeyUp={(e) => {
+ if (disabled) return;
+ if (RELEASE_KEYS.includes(e.key as (typeof RELEASE_KEYS)[number])) {
+ onRelease?.(Number(e.currentTarget.value));
+ onDragEnd?.();
+ }
+ }}
+ />
+
+ );
+}
diff --git a/client/src/pages/Home.tsx b/client/src/pages/Home.tsx
index 0ed2e9c..8d39fe6 100644
--- a/client/src/pages/Home.tsx
+++ b/client/src/pages/Home.tsx
@@ -12,6 +12,7 @@
import { useDevice } from "@/contexts/DeviceContext";
import ConnectionPlaceholder from "@/components/ConnectionPlaceholder";
import BottomNav from "@/components/BottomNav";
+import ThumbOnlyRangeSlider from "@/components/ThumbOnlyRangeSlider";
import {
AlertDialog,
AlertDialogAction,
@@ -231,8 +232,6 @@ export default function Home() {
const [localVol, setLocalVol] = useState(deviceState?.volume ?? 100);
const isDragging = useRef(false);
- const volumeSliderRef = useRef(null);
- const volumeSliderDragAllowedRef = useRef(false);
const knobDraggingRef = useRef(false);
const knobActivePointerIdRef = useRef(null);
const knobStartAngleRef = useRef(0);
@@ -249,16 +248,6 @@ export default function Home() {
knobLastVolRef.current = localVol;
}, [localVol]);
- const isPointerOnVolumeSliderThumb = useCallback((clientX: number) => {
- const el = volumeSliderRef.current;
- if (!el) return false;
- const rect = el.getBoundingClientRect();
- if (rect.width <= 0) return false;
- const thumbCenterX = rect.left + (localVol / 200) * rect.width;
- const hitRadius = Math.max(28, rect.width * 0.05);
- return Math.abs(clientX - thumbCenterX) <= hitRadius;
- }, [localVol]);
-
const handlePowerOff = useCallback(async () => {
if (powerActionRef.current) return;
powerActionRef.current = true;
@@ -677,67 +666,26 @@ export default function Home() {
background: "#00FFF6",
boxShadow: "0 0 8px rgba(0,255,246,0.5)"
}} />
- {
+ className={volumePassthroughActive ? "opacity-50" : undefined}
+ onChange={(v) => {
if (volumePassthroughActive) return;
- if (!volumeSliderDragAllowedRef.current) {
- e.target.value = String(localVol);
- return;
- }
- const v = Number(e.target.value);
setLocalVol(v);
}}
- onPointerDown={(e) => {
+ onDragStart={() => {
if (volumePassthroughActive) return;
- const onThumb = isPointerOnVolumeSliderThumb(e.clientX);
- volumeSliderDragAllowedRef.current = onThumb;
- if (!onThumb) {
- e.preventDefault();
- return;
- }
isDragging.current = true;
}}
- onPointerUp={(e) => {
- const allowed = volumeSliderDragAllowedRef.current;
- volumeSliderDragAllowedRef.current = false;
+ onDragEnd={() => {
isDragging.current = false;
- if (!allowed || volumePassthroughActive) return;
- applyVolume(Number(e.currentTarget.value));
}}
- onPointerCancel={(e) => {
- const allowed = volumeSliderDragAllowedRef.current;
- volumeSliderDragAllowedRef.current = false;
- isDragging.current = false;
- if (!volumePassthroughActive && allowed) {
- applyVolume(Number(e.currentTarget.value));
- }
- }}
- onBlur={(e) => {
- const allowed = volumeSliderDragAllowedRef.current;
- volumeSliderDragAllowedRef.current = false;
- isDragging.current = false;
- if (!volumePassthroughActive && allowed) {
- applyVolume(Number(e.currentTarget.value));
- }
- }}
- onKeyUp={(e) => {
- const k = e.key;
- if (
- k === "ArrowLeft" ||
- k === "ArrowRight" ||
- k === "ArrowUp" ||
- k === "ArrowDown" ||
- k === "Home" ||
- k === "End" ||
- k === "PageUp" ||
- k === "PageDown"
- ) {
- applyVolume(Number(e.currentTarget.value));
- }
+ onRelease={(v) => {
+ if (volumePassthroughActive) return;
+ applyVolume(v);
}}
/>
diff --git a/client/src/pages/eq/components/BrandDrawer.tsx b/client/src/pages/eq/components/BrandDrawer.tsx
index f5eadcd..14bdd44 100644
--- a/client/src/pages/eq/components/BrandDrawer.tsx
+++ b/client/src/pages/eq/components/BrandDrawer.tsx
@@ -164,7 +164,7 @@ export function BrandDrawer({
onClick={onClose}
/>