Files
audio-ui/components/tracks/TrackFader.tsx
Sebastian Krüger 5d9e02fe95 feat: streamline track and master controls layout consistency
- Streamlined track controls and master controls to same width (240px)
- Fixed track controls container to use full width of parent column
- Matched TrackControls card structure with MasterControls (gap-3, no w-full/h-full)
- Updated outer container padding from p-2 to p-4 with gap-4
- Adjusted track controls wrapper to center content instead of stretching
- Added max-width constraint to PlaybackControls to prevent width changes
- Centered transport control buttons in footer

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-19 16:32:49 +01:00

236 lines
7.6 KiB
TypeScript

'use client';
import * as React from 'react';
import { cn } from '@/lib/utils/cn';
export interface TrackFaderProps {
value: number;
peakLevel: number;
rmsLevel: number;
onChange: (value: number) => void;
onTouchStart?: () => void;
onTouchEnd?: () => void;
className?: string;
}
export function TrackFader({
value,
peakLevel,
rmsLevel,
onChange,
onTouchStart,
onTouchEnd,
className,
}: TrackFaderProps) {
const [isDragging, setIsDragging] = React.useState(false);
const containerRef = React.useRef<HTMLDivElement>(null);
// Convert linear 0-1 to dB scale for display
const linearToDb = (linear: number): number => {
if (linear === 0) return -60;
const db = 20 * Math.log10(linear);
return Math.max(-60, Math.min(0, db));
};
const valueDb = linearToDb(value);
const peakDb = linearToDb(peakLevel);
const rmsDb = linearToDb(rmsLevel);
// Calculate bar widths (0-100%)
const peakWidth = ((peakDb + 60) / 60) * 100;
const rmsWidth = ((rmsDb + 60) / 60) * 100;
const handleMouseDown = (e: React.MouseEvent) => {
e.preventDefault();
setIsDragging(true);
onTouchStart?.();
updateValue(e.clientY);
};
const handleMouseMove = React.useCallback(
(e: MouseEvent) => {
if (!isDragging) return;
updateValue(e.clientY);
},
[isDragging]
);
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;
const rect = containerRef.current.getBoundingClientRect();
const y = clientY - rect.top;
// Inverted: top = max (1), bottom = min (0)
const percentage = Math.max(0, Math.min(1, 1 - (y / rect.height)));
onChange(percentage);
};
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, handleTouchMove, handleTouchEnd]);
return (
<div className={cn('flex gap-3', className)} style={{ marginLeft: '16px' }}>
{/* dB Labels (Left) */}
<div className="flex flex-col justify-between text-[10px] font-mono text-muted-foreground py-1">
<span>0</span>
<span>-12</span>
<span>-24</span>
<span>-60</span>
</div>
{/* Fader Container */}
<div
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) */}
<div className="absolute inset-x-2 top-2 h-3 bg-background/80 rounded-sm overflow-hidden border border-border/30">
<div
className="absolute left-0 top-0 bottom-0 transition-all duration-75 ease-out"
style={{ width: `${Math.max(0, Math.min(100, peakWidth))}%` }}
>
<div className={cn(
'w-full h-full',
peakDb > -3 ? 'bg-red-500' :
peakDb > -6 ? 'bg-yellow-500' :
'bg-green-500'
)} />
</div>
</div>
{/* RMS Meter (Horizontal Bar - Bottom) */}
<div className="absolute inset-x-2 bottom-2 h-3 bg-background/80 rounded-sm overflow-hidden border border-border/30">
<div
className="absolute left-0 top-0 bottom-0 transition-all duration-150 ease-out"
style={{ width: `${Math.max(0, Math.min(100, rmsWidth))}%` }}
>
<div className={cn(
'w-full h-full',
rmsDb > -3 ? 'bg-red-500' :
rmsDb > -6 ? 'bg-yellow-500' :
'bg-green-500'
)} />
</div>
</div>
{/* Fader Track */}
<div className="absolute top-8 bottom-8 left-1/2 -translate-x-1/2 w-1.5 bg-muted/50 rounded-full" />
{/* Fader Handle */}
<div
className="absolute left-1/2 -translate-x-1/2 w-10 h-4 bg-primary/80 border-2 border-primary rounded-md shadow-lg cursor-grab active:cursor-grabbing pointer-events-none transition-all"
style={{
// Inverted: value 1 = top, value 0 = bottom
top: `calc(${(1 - value) * 100}% - 0.5rem)`,
}}
>
{/* Handle grip lines */}
<div className="absolute inset-0 flex items-center justify-center gap-0.5">
<div className="h-2 w-px bg-primary-foreground/30" />
<div className="h-2 w-px bg-primary-foreground/30" />
<div className="h-2 w-px bg-primary-foreground/30" />
</div>
</div>
{/* dB Scale Markers */}
<div className="absolute inset-0 px-2 py-8 pointer-events-none">
<div className="relative h-full">
{/* -12 dB */}
<div className="absolute left-0 right-0 h-px bg-border/20" style={{ top: '50%' }} />
{/* -6 dB */}
<div className="absolute left-0 right-0 h-px bg-yellow-500/20" style={{ top: '20%' }} />
{/* -3 dB */}
<div className="absolute left-0 right-0 h-px bg-red-500/30" style={{ top: '10%' }} />
</div>
</div>
</div>
{/* Value and Level Display (Right) */}
<div className="flex flex-col justify-between items-start text-[9px] font-mono py-1 w-[36px]">
{/* Current dB Value */}
<div className={cn(
'font-bold text-[11px]',
valueDb > -3 ? 'text-red-500' :
valueDb > -6 ? 'text-yellow-500' :
'text-green-500'
)}>
{valueDb > -60 ? `${valueDb.toFixed(1)}` : '-∞'}
</div>
{/* Spacer */}
<div className="flex-1" />
{/* Peak Level */}
<div className="flex flex-col items-start">
<span className="text-muted-foreground/60">PK</span>
<span className={cn(
'font-mono text-[10px]',
peakDb > -3 ? 'text-red-500' :
peakDb > -6 ? 'text-yellow-500' :
'text-green-500'
)}>
{peakDb > -60 ? `${peakDb.toFixed(1)}` : '-∞'}
</span>
</div>
{/* RMS Level */}
<div className="flex flex-col items-start">
<span className="text-muted-foreground/60">RM</span>
<span className={cn(
'font-mono text-[10px]',
rmsDb > -3 ? 'text-red-500' :
rmsDb > -6 ? 'text-yellow-500' :
'text-green-500'
)}>
{rmsDb > -60 ? `${rmsDb.toFixed(1)}` : '-∞'}
</span>
</div>
{/* dB Label */}
<span className="text-muted-foreground/60 text-[8px]">dB</span>
</div>
</div>
);
}