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>
This commit is contained in:
2025-11-19 00:08:36 +01:00
parent c33a77270b
commit 441920ee70
7 changed files with 619 additions and 103 deletions

View File

@@ -35,6 +35,8 @@ export function AudioEditor() {
const [selectedTrackId, setSelectedTrackId] = React.useState<string | null>(null);
const [zoom, setZoom] = React.useState(1);
const [masterVolume, setMasterVolume] = React.useState(0.8);
const [masterPan, setMasterPan] = React.useState(0);
const [isMasterMuted, setIsMasterMuted] = React.useState(false);
const [clipboard, setClipboard] = React.useState<AudioBuffer | null>(null);
const [recordingTrackId, setRecordingTrackId] = React.useState<string | null>(null);
const [punchInEnabled, setPunchInEnabled] = React.useState(false);
@@ -1042,11 +1044,22 @@ export function AudioEditor() {
currentTime={currentTime}
duration={duration}
volume={masterVolume}
pan={masterPan}
onPlay={play}
onPause={pause}
onStop={stop}
onSeek={seek}
onVolumeChange={setMasterVolume}
onPanChange={setMasterPan}
onMuteToggle={() => {
if (isMasterMuted) {
setMasterVolume(0.8);
setIsMasterMuted(false);
} else {
setMasterVolume(0);
setIsMasterMuted(true);
}
}}
currentTimeFormatted={formatDuration(currentTime)}
durationFormatted={formatDuration(duration)}
isRecording={recordingState.isRecording}

View File

@@ -1,10 +1,9 @@
'use client';
import * as React from 'react';
import { Play, Pause, Square, SkipBack, Volume2, VolumeX, Circle, AlignVerticalJustifyStart, AlignVerticalJustifyEnd, Layers } from 'lucide-react';
import { Play, Pause, Square, SkipBack, Circle, AlignVerticalJustifyStart, AlignVerticalJustifyEnd, Layers } from 'lucide-react';
import { Button } from '@/components/ui/Button';
import { Slider } from '@/components/ui/Slider';
import { MasterMeter } from '@/components/controls/MasterMeter';
import { MasterControls } from '@/components/controls/MasterControls';
import { cn } from '@/lib/utils/cn';
export interface PlaybackControlsProps {
@@ -13,11 +12,14 @@ export interface PlaybackControlsProps {
currentTime: number;
duration: number;
volume: number;
pan?: number;
onPlay: () => void;
onPause: () => void;
onStop: () => void;
onSeek: (time: number, autoPlay?: boolean) => void;
onVolumeChange: (volume: number) => void;
onPanChange?: (pan: number) => void;
onMuteToggle?: () => void;
disabled?: boolean;
className?: string;
currentTimeFormatted?: string;
@@ -45,11 +47,14 @@ export function PlaybackControls({
currentTime,
duration,
volume,
pan = 0,
onPlay,
onPause,
onStop,
onSeek,
onVolumeChange,
onPanChange,
onMuteToggle,
disabled = false,
className,
currentTimeFormatted,
@@ -70,9 +75,6 @@ export function PlaybackControls({
masterIsClipping = false,
onResetClip,
}: PlaybackControlsProps) {
const [isMuted, setIsMuted] = React.useState(false);
const [previousVolume, setPreviousVolume] = React.useState(volume);
const handlePlayPause = () => {
if (isPlaying) {
onPause();
@@ -81,26 +83,6 @@ export function PlaybackControls({
}
};
const handleMuteToggle = () => {
if (isMuted) {
onVolumeChange(previousVolume);
setIsMuted(false);
} else {
setPreviousVolume(volume);
onVolumeChange(0);
setIsMuted(true);
}
};
const handleVolumeChange = (newVolume: number) => {
onVolumeChange(newVolume);
if (newVolume === 0) {
setIsMuted(true);
} else {
setIsMuted(false);
}
};
const progress = duration > 0 ? (currentTime / duration) * 100 : 0;
return (
@@ -284,41 +266,21 @@ export function PlaybackControls({
)}
</div>
{/* Volume Control & Master Meter */}
<div className="flex items-center gap-4">
{/* Master Meter */}
<MasterMeter
{/* Master Controls */}
{onPanChange && onMuteToggle && (
<MasterControls
volume={volume}
pan={pan}
peakLevel={masterPeakLevel}
rmsLevel={masterRmsLevel}
isClipping={masterIsClipping}
isMuted={volume === 0}
onVolumeChange={onVolumeChange}
onPanChange={onPanChange}
onMuteToggle={onMuteToggle}
onResetClip={onResetClip}
/>
{/* Volume Control */}
<div className="flex items-center gap-3 min-w-[200px]">
<Button
variant="ghost"
size="icon"
onClick={handleMuteToggle}
title={isMuted ? 'Unmute' : 'Mute'}
>
{isMuted || volume === 0 ? (
<VolumeX className="h-5 w-5" />
) : (
<Volume2 className="h-5 w-5" />
)}
</Button>
<Slider
value={volume}
onChange={handleVolumeChange}
min={0}
max={1}
step={0.01}
className="flex-1"
/>
</div>
</div>
)}
</div>
</div>
);