From 418c79d961571df7dd4a8171b68a57885f20e749 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Kr=C3=BCger?= Date: Wed, 19 Nov 2025 01:19:03 +0100 Subject: [PATCH] feat: add touch event support to knobs and faders MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Added comprehensive touch event handling for mobile/tablet support: CircularKnob.tsx: - Added handleTouchStart, handleTouchMove, handleTouchEnd handlers - Touch events use same drag logic as mouse events - Prevents default to avoid scrolling while adjusting TrackFader.tsx: - Added touch event handlers for vertical fader control - Integrated with existing onTouchStart/onTouchEnd callbacks - Supports touch-based automation recording MasterFader.tsx: - Added touch event handlers matching TrackFader - Complete touch support for master volume control All components now work seamlessly on touch-enabled devices. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- components/controls/MasterFader.tsx | 37 +++++++++++++++++++++++++++-- components/tracks/TrackFader.tsx | 29 +++++++++++++++++++++- components/ui/CircularKnob.tsx | 37 ++++++++++++++++++++++++++++- 3 files changed, 99 insertions(+), 4 deletions(-) diff --git a/components/controls/MasterFader.tsx b/components/controls/MasterFader.tsx index 67cb569..91d95b2 100644 --- a/components/controls/MasterFader.tsx +++ b/components/controls/MasterFader.tsx @@ -10,6 +10,8 @@ export interface MasterFaderProps { isClipping: boolean; onChange: (value: number) => void; onResetClip?: () => void; + onTouchStart?: () => void; + onTouchEnd?: () => void; className?: string; } @@ -20,6 +22,8 @@ export function MasterFader({ isClipping, onChange, onResetClip, + onTouchStart, + onTouchEnd, className, }: MasterFaderProps) { const [isDragging, setIsDragging] = React.useState(false); @@ -43,6 +47,7 @@ export function MasterFader({ const handleMouseDown = (e: React.MouseEvent) => { e.preventDefault(); setIsDragging(true); + onTouchStart?.(); updateValue(e.clientY); }; @@ -56,7 +61,30 @@ export function MasterFader({ const handleMouseUp = React.useCallback(() => { setIsDragging(false); - }, []); + onTouchEnd?.(); + }, [onTouchEnd]); + + const handleTouchStart = (e: React.TouchEvent) => { + e.preventDefault(); + const touch = e.touches[0]; + setIsDragging(true); + onTouchStart?.(); + updateValue(touch.clientY); + }; + + const handleTouchMove = React.useCallback( + (e: TouchEvent) => { + if (!isDragging || e.touches.length === 0) return; + const touch = e.touches[0]; + updateValue(touch.clientY); + }, + [isDragging] + ); + + const handleTouchEnd = React.useCallback(() => { + setIsDragging(false); + onTouchEnd?.(); + }, [onTouchEnd]); const updateValue = (clientY: number) => { if (!containerRef.current) return; @@ -72,12 +100,16 @@ export function MasterFader({ if (isDragging) { window.addEventListener('mousemove', handleMouseMove); window.addEventListener('mouseup', handleMouseUp); + window.addEventListener('touchmove', handleTouchMove); + window.addEventListener('touchend', handleTouchEnd); return () => { window.removeEventListener('mousemove', handleMouseMove); window.removeEventListener('mouseup', handleMouseUp); + window.removeEventListener('touchmove', handleTouchMove); + window.removeEventListener('touchend', handleTouchEnd); }; } - }, [isDragging, handleMouseMove, handleMouseUp]); + }, [isDragging, handleMouseMove, handleMouseUp, handleTouchMove, handleTouchEnd]); return (
@@ -94,6 +126,7 @@ export function MasterFader({ ref={containerRef} className="relative w-12 h-40 bg-background/50 rounded-md border border-border/50 cursor-pointer" onMouseDown={handleMouseDown} + onTouchStart={handleTouchStart} > {/* Peak Meter (Horizontal Bar - Top) */}
diff --git a/components/tracks/TrackFader.tsx b/components/tracks/TrackFader.tsx index c1c1ff2..c2af1aa 100644 --- a/components/tracks/TrackFader.tsx +++ b/components/tracks/TrackFader.tsx @@ -60,6 +60,28 @@ export function TrackFader({ onTouchEnd?.(); }, [onTouchEnd]); + const handleTouchStart = (e: React.TouchEvent) => { + e.preventDefault(); + const touch = e.touches[0]; + setIsDragging(true); + onTouchStart?.(); + updateValue(touch.clientY); + }; + + const handleTouchMove = React.useCallback( + (e: TouchEvent) => { + if (!isDragging || e.touches.length === 0) return; + const touch = e.touches[0]; + updateValue(touch.clientY); + }, + [isDragging] + ); + + const handleTouchEnd = React.useCallback(() => { + setIsDragging(false); + onTouchEnd?.(); + }, [onTouchEnd]); + const updateValue = (clientY: number) => { if (!containerRef.current) return; @@ -74,12 +96,16 @@ export function TrackFader({ if (isDragging) { window.addEventListener('mousemove', handleMouseMove); window.addEventListener('mouseup', handleMouseUp); + window.addEventListener('touchmove', handleTouchMove); + window.addEventListener('touchend', handleTouchEnd); return () => { window.removeEventListener('mousemove', handleMouseMove); window.removeEventListener('mouseup', handleMouseUp); + window.removeEventListener('touchmove', handleTouchMove); + window.removeEventListener('touchend', handleTouchEnd); }; } - }, [isDragging, handleMouseMove, handleMouseUp]); + }, [isDragging, handleMouseMove, handleMouseUp, handleTouchMove, handleTouchEnd]); return (
@@ -96,6 +122,7 @@ export function TrackFader({ ref={containerRef} className="relative w-10 h-32 bg-background/50 rounded-md border border-border/50 cursor-pointer" onMouseDown={handleMouseDown} + onTouchStart={handleTouchStart} > {/* Peak Meter (Horizontal Bar - Top) */}
diff --git a/components/ui/CircularKnob.tsx b/components/ui/CircularKnob.tsx index bfa4cbf..300efc5 100644 --- a/components/ui/CircularKnob.tsx +++ b/components/ui/CircularKnob.tsx @@ -91,17 +91,51 @@ export function CircularKnob({ onTouchEnd?.(); }, [onTouchEnd]); + const handleTouchStart = React.useCallback( + (e: React.TouchEvent) => { + e.preventDefault(); + const touch = e.touches[0]; + setIsDragging(true); + dragStartRef.current = { + x: touch.clientX, + y: touch.clientY, + value, + }; + onTouchStart?.(); + }, + [value, onTouchStart] + ); + + const handleTouchMove = React.useCallback( + (e: TouchEvent) => { + if (isDragging && e.touches.length > 0) { + const touch = e.touches[0]; + updateValue(touch.clientX, touch.clientY); + } + }, + [isDragging, updateValue] + ); + + const handleTouchEnd = React.useCallback(() => { + setIsDragging(false); + onTouchEnd?.(); + }, [onTouchEnd]); + React.useEffect(() => { if (isDragging) { window.addEventListener('mousemove', handleMouseMove); window.addEventListener('mouseup', handleMouseUp); + window.addEventListener('touchmove', handleTouchMove); + window.addEventListener('touchend', handleTouchEnd); return () => { window.removeEventListener('mousemove', handleMouseMove); window.removeEventListener('mouseup', handleMouseUp); + window.removeEventListener('touchmove', handleTouchMove); + window.removeEventListener('touchend', handleTouchEnd); }; } - }, [isDragging, handleMouseMove, handleMouseUp]); + }, [isDragging, handleMouseMove, handleMouseUp, handleTouchMove, handleTouchEnd]); // Calculate rotation angle (-135deg to 135deg, 270deg range) const percentage = (value - min) / (max - min); @@ -148,6 +182,7 @@ export function CircularKnob({