Files
audio-ui/components/tracks/TrackFader.tsx
Sebastian Krüger 441920ee70 feat: redesign track and master controls with integrated fader+meters+pan
Unified design language across all tracks and master section:
- Created TrackFader component: vertical fader with horizontal meter bars
- Created TrackControls: integrated pan + fader + mute in compact layout
- Created MasterFader: similar design but larger for master output
- Created MasterControls: master version with pan + fader + mute
- Updated Track component to use new TrackControls
- Updated PlaybackControls to use new MasterControls
- Removed old VerticalFader and separate meter components

New features:
- Horizontal peak/RMS meter bars behind fader (top=peak, bottom=RMS)
- Color-coded meters (green/yellow/red based on dB levels)
- dB scale labels and numeric readouts
- Integrated mute button in controls
- Consistent circular pan knobs
- Professional DAW-style channel strip appearance
- Master section includes clip indicator

Visual improvements:
- Unified design across all tracks and master
- Compact vertical layout saves space
- Real-time level monitoring integrated with volume control
- Smooth animations and transitions

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-19 00:08:36 +01:00

177 lines
5.7 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 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);
return () => {
window.removeEventListener('mousemove', handleMouseMove);
window.removeEventListener('mouseup', handleMouseUp);
};
}
}, [isDragging, handleMouseMove, handleMouseUp]);
return (
<div className={cn('flex gap-2', className)}>
{/* dB Labels (Left) */}
<div className="flex flex-col justify-between text-[9px] 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-10 h-32 bg-background/50 rounded-md border border-border/50 cursor-pointer"
onMouseDown={handleMouseDown}
>
{/* Peak Meter (Horizontal Bar - Top) */}
<div className="absolute inset-x-1.5 top-1.5 h-2.5 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-1.5 bottom-1.5 h-2.5 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-400' :
rmsDb > -6 ? 'bg-yellow-400' :
'bg-green-400'
)} />
</div>
</div>
{/* Fader Track */}
<div className="absolute top-6 bottom-6 left-1/2 -translate-x-1/2 w-1 bg-muted/50 rounded-full" />
{/* Fader Handle */}
<div
className="absolute left-1/2 -translate-x-1/2 w-9 h-3.5 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.4375rem)`,
}}
>
{/* Handle grip lines */}
<div className="absolute inset-0 flex items-center justify-center gap-0.5">
<div className="h-1.5 w-px bg-primary-foreground/30" />
<div className="h-1.5 w-px bg-primary-foreground/30" />
<div className="h-1.5 w-px bg-primary-foreground/30" />
</div>
</div>
{/* dB Scale Markers */}
<div className="absolute inset-0 px-1.5 py-6 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 Display (Right) */}
<div className="flex flex-col justify-center items-start text-[9px] font-mono">
{/* Current dB Value */}
<div className={cn(
'font-bold text-[10px]',
valueDb > -3 ? 'text-red-500' :
valueDb > -6 ? 'text-yellow-500' :
'text-green-500'
)}>
{valueDb > -60 ? `${valueDb.toFixed(1)}` : '-∞'}
</div>
</div>
</div>
);
}