feat: redesign track layout to DAW-style with left control panel
Major UX improvement inspired by Audacity/Ableton Live: - Each track now has 2 sections: left control panel (fixed 288px) and right waveform (flexible) - Left panel contains: track name, color indicator, collapse toggle, volume, pan, solo, mute, delete - TrackHeader component functionality moved directly into Track component - Removed redundant track controls from SidePanel - SidePanel now simplified to show global actions and effect chain - Track controls are always visible on the left, waveform scrolls horizontally on the right - Collapsed tracks show only the header row (48px height) - Much better UX for multi-track editing 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -9,13 +9,10 @@ import {
|
||||
Trash2,
|
||||
Link2,
|
||||
FolderOpen,
|
||||
Volume2,
|
||||
Music2,
|
||||
} from 'lucide-react';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { Slider } from '@/components/ui/Slider';
|
||||
import { cn } from '@/lib/utils/cn';
|
||||
import { formatDuration } from '@/lib/audio/decoder';
|
||||
import type { Track } from '@/types/track';
|
||||
import type { EffectChain, EffectPreset } from '@/lib/audio/effects/chain';
|
||||
import { EffectRack } from '@/components/effects/EffectRack';
|
||||
@@ -165,122 +162,17 @@ export function SidePanel({
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Track List */}
|
||||
{/* Track List - Simplified */}
|
||||
{tracks.length > 0 ? (
|
||||
<div className="space-y-2">
|
||||
<h3 className="text-xs font-semibold text-muted-foreground uppercase">
|
||||
Tracks ({tracks.length})
|
||||
</h3>
|
||||
<div className="space-y-2">
|
||||
{tracks.map((track) => {
|
||||
const isSelected = selectedTrackId === track.id;
|
||||
return (
|
||||
<div
|
||||
key={track.id}
|
||||
className={cn(
|
||||
'p-3 rounded-lg border transition-colors cursor-pointer',
|
||||
isSelected
|
||||
? 'bg-primary/10 border-primary'
|
||||
: 'bg-secondary/30 border-border hover:border-primary/50'
|
||||
)}
|
||||
onClick={() => onSelectTrack(isSelected ? null : track.id)}
|
||||
>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="text-sm font-medium text-foreground truncate">
|
||||
{String(track.name || 'Untitled Track')}
|
||||
</div>
|
||||
{track.audioBuffer && (
|
||||
<div className="text-xs text-muted-foreground">
|
||||
{formatDuration(track.audioBuffer.duration)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon-sm"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onRemoveTrack(track.id);
|
||||
}}
|
||||
title="Remove track"
|
||||
>
|
||||
<Trash2 className="h-3.5 w-3.5 text-destructive" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Track Controls - Always visible */}
|
||||
<div className="space-y-2" onClick={(e) => e.stopPropagation()}>
|
||||
{/* Volume */}
|
||||
<div className="space-y-1">
|
||||
<div className="flex items-center justify-between">
|
||||
<label className="text-xs text-muted-foreground flex items-center gap-1">
|
||||
<Volume2 className="h-3 w-3" />
|
||||
Volume
|
||||
</label>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{Math.round(track.volume * 100)}%
|
||||
</span>
|
||||
</div>
|
||||
<Slider
|
||||
value={track.volume}
|
||||
onChange={(value) => onUpdateTrack(track.id, { volume: value })}
|
||||
min={0}
|
||||
max={1}
|
||||
step={0.01}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Pan */}
|
||||
<div className="space-y-1">
|
||||
<div className="flex items-center justify-between">
|
||||
<label className="text-xs text-muted-foreground">Pan</label>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{track.pan === 0
|
||||
? 'C'
|
||||
: track.pan < 0
|
||||
? `L${Math.round(Math.abs(track.pan) * 100)}`
|
||||
: `R${Math.round(track.pan * 100)}`}
|
||||
</span>
|
||||
</div>
|
||||
<Slider
|
||||
value={track.pan}
|
||||
onChange={(value) => onUpdateTrack(track.id, { pan: value })}
|
||||
min={-1}
|
||||
max={1}
|
||||
step={0.01}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Solo / Mute */}
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant={track.solo ? 'default' : 'outline'}
|
||||
size="sm"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onUpdateTrack(track.id, { solo: !track.solo });
|
||||
}}
|
||||
className="flex-1 text-xs"
|
||||
>
|
||||
Solo
|
||||
</Button>
|
||||
<Button
|
||||
variant={track.mute ? 'default' : 'outline'}
|
||||
size="sm"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onUpdateTrack(track.id, { mute: !track.mute });
|
||||
}}
|
||||
className="flex-1 text-xs"
|
||||
>
|
||||
Mute
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<div className="text-xs text-muted-foreground">
|
||||
<p className="mb-2">
|
||||
Track controls are located on the left side of each track in the timeline.
|
||||
</p>
|
||||
<p>Click a track to select it and apply effects from the Effect Chain tab.</p>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
|
||||
Reference in New Issue
Block a user