refactor: improve UX with 2-tab sidebar layout (Option A)
Changed from confusing 3-tab layout to cleaner 2-tab design inspired by professional DAWs like Ableton Live: **Tracks Tab:** - Track management actions (Add/Import/Clear) - Track count with selected track indicator - Contextual help text - Selected track's effects shown below (when track is selected) - Clear visual separation with border-top **Master Tab:** - Master channel description - Master effects chain - Preset management for master effects - Clear that these apply to final mix Benefits: - Clear separation: track operations vs master operations - Contextual: selecting a track shows its effects in same tab - Less cognitive load than 3 tabs - Follows DAW conventions (track strip vs master strip) - Selected track name shown in status area 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -75,7 +75,7 @@ export function SidePanel({
|
|||||||
className,
|
className,
|
||||||
}: SidePanelProps) {
|
}: SidePanelProps) {
|
||||||
const [isCollapsed, setIsCollapsed] = React.useState(false);
|
const [isCollapsed, setIsCollapsed] = React.useState(false);
|
||||||
const [activeTab, setActiveTab] = React.useState<'tracks' | 'trackFx' | 'masterFx'>('tracks');
|
const [activeTab, setActiveTab] = React.useState<'tracks' | 'master'>('tracks');
|
||||||
const [presetDialogOpen, setPresetDialogOpen] = React.useState(false);
|
const [presetDialogOpen, setPresetDialogOpen] = React.useState(false);
|
||||||
|
|
||||||
const selectedTrack = tracks.find((t) => t.id === selectedTrackId);
|
const selectedTrack = tracks.find((t) => t.id === selectedTrackId);
|
||||||
@@ -107,27 +107,21 @@ export function SidePanel({
|
|||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<Button
|
<Button
|
||||||
variant={activeTab === 'tracks' ? 'secondary' : 'ghost'}
|
variant={activeTab === 'tracks' ? 'secondary' : 'ghost'}
|
||||||
size="icon-sm"
|
size="sm"
|
||||||
onClick={() => setActiveTab('tracks')}
|
onClick={() => setActiveTab('tracks')}
|
||||||
title="Tracks"
|
title="Tracks"
|
||||||
>
|
>
|
||||||
<Music2 className="h-4 w-4" />
|
<Music2 className="h-4 w-4 mr-1.5" />
|
||||||
|
Tracks
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant={activeTab === 'trackFx' ? 'secondary' : 'ghost'}
|
variant={activeTab === 'master' ? 'secondary' : 'ghost'}
|
||||||
size="icon-sm"
|
size="sm"
|
||||||
onClick={() => setActiveTab('trackFx')}
|
onClick={() => setActiveTab('master')}
|
||||||
title="Track Effects"
|
title="Master"
|
||||||
>
|
>
|
||||||
<Link2 className="h-4 w-4" />
|
<Link2 className="h-4 w-4 mr-1.5 text-primary" />
|
||||||
</Button>
|
Master
|
||||||
<Button
|
|
||||||
variant={activeTab === 'masterFx' ? 'secondary' : 'ghost'}
|
|
||||||
size="icon-sm"
|
|
||||||
onClick={() => setActiveTab('masterFx')}
|
|
||||||
title="Master Effects"
|
|
||||||
>
|
|
||||||
<Link2 className="h-4 w-4 text-primary" />
|
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
@@ -147,7 +141,7 @@ export function SidePanel({
|
|||||||
{/* Track Actions */}
|
{/* Track Actions */}
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<h3 className="text-xs font-semibold text-muted-foreground uppercase">
|
<h3 className="text-xs font-semibold text-muted-foreground uppercase">
|
||||||
Multi-Track Editor
|
Track Management
|
||||||
</h3>
|
</h3>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<Button
|
<Button
|
||||||
@@ -182,17 +176,25 @@ export function SidePanel({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Track List - Simplified */}
|
{/* Track List Summary */}
|
||||||
{tracks.length > 0 ? (
|
{tracks.length > 0 ? (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<h3 className="text-xs font-semibold text-muted-foreground uppercase">
|
<div className="flex items-center justify-between">
|
||||||
Tracks ({tracks.length})
|
<h3 className="text-xs font-semibold text-muted-foreground uppercase">
|
||||||
</h3>
|
Tracks ({tracks.length})
|
||||||
|
</h3>
|
||||||
|
{selectedTrack && (
|
||||||
|
<span className="text-xs text-primary">
|
||||||
|
{String(selectedTrack.name || 'Untitled Track')} selected
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
<div className="text-xs text-muted-foreground">
|
<div className="text-xs text-muted-foreground">
|
||||||
<p className="mb-2">
|
<p>
|
||||||
Track controls are located on the left side of each track in the timeline.
|
{selectedTrack
|
||||||
|
? 'Track controls are on the left of each track. Effects for the selected track are shown below.'
|
||||||
|
: 'Click a track\'s waveform to select it and edit its effects below.'}
|
||||||
</p>
|
</p>
|
||||||
<p>Click a track to select it and apply effects from the Effect Chain tab.</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
@@ -203,96 +205,97 @@ export function SidePanel({
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Selected Track Effects */}
|
||||||
|
{selectedTrack && (
|
||||||
|
<div className="space-y-2 pt-3 border-t border-border">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<h3 className="text-xs font-semibold text-muted-foreground uppercase">
|
||||||
|
Track Effects
|
||||||
|
</h3>
|
||||||
|
{trackEffectChain && trackEffectChain.effects.length > 0 && (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon-sm"
|
||||||
|
onClick={onClearTrackChain}
|
||||||
|
title="Clear all effects"
|
||||||
|
>
|
||||||
|
<Trash2 className="h-4 w-4 text-destructive" />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<EffectRack
|
||||||
|
chain={trackEffectChain!}
|
||||||
|
onToggleEffect={onToggleTrackEffect}
|
||||||
|
onRemoveEffect={onRemoveTrackEffect}
|
||||||
|
onReorderEffects={onReorderTrackEffects}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{activeTab === 'trackFx' && (
|
{activeTab === 'master' && (
|
||||||
<div className="space-y-2">
|
<>
|
||||||
<div className="flex items-center justify-between">
|
{/* Master Channel Info */}
|
||||||
|
<div className="space-y-2">
|
||||||
<h3 className="text-xs font-semibold text-muted-foreground uppercase">
|
<h3 className="text-xs font-semibold text-muted-foreground uppercase">
|
||||||
Track Effects
|
Master Channel
|
||||||
{selectedTrack && (
|
|
||||||
<span className="text-primary ml-2">({String(selectedTrack.name || 'Untitled Track')})</span>
|
|
||||||
)}
|
|
||||||
</h3>
|
</h3>
|
||||||
<div className="flex gap-1">
|
<div className="text-xs text-muted-foreground">
|
||||||
{trackEffectChain && trackEffectChain.effects.length > 0 && (
|
<p>
|
||||||
<Button
|
Master effects are applied to the final mix of all tracks.
|
||||||
variant="ghost"
|
|
||||||
size="icon-sm"
|
|
||||||
onClick={onClearTrackChain}
|
|
||||||
title="Clear all effects"
|
|
||||||
>
|
|
||||||
<Trash2 className="h-4 w-4 text-destructive" />
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{!selectedTrack ? (
|
|
||||||
<div className="text-center py-8">
|
|
||||||
<Link2 className="h-12 w-12 mx-auto text-muted-foreground/50 mb-2" />
|
|
||||||
<p className="text-sm text-muted-foreground">
|
|
||||||
Select a track to apply effects
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
</div>
|
||||||
<EffectRack
|
|
||||||
chain={trackEffectChain!}
|
|
||||||
onToggleEffect={onToggleTrackEffect}
|
|
||||||
onRemoveEffect={onRemoveTrackEffect}
|
|
||||||
onReorderEffects={onReorderTrackEffects}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{activeTab === 'masterFx' && (
|
{/* Master Effects */}
|
||||||
<div className="space-y-2">
|
<div className="space-y-2 pt-3 border-t border-border">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<h3 className="text-xs font-semibold text-muted-foreground uppercase">
|
<h3 className="text-xs font-semibold text-muted-foreground uppercase">
|
||||||
Master Effects
|
Master Effects
|
||||||
</h3>
|
</h3>
|
||||||
<div className="flex gap-1">
|
<div className="flex gap-1">
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="icon-sm"
|
|
||||||
onClick={() => setPresetDialogOpen(true)}
|
|
||||||
title="Manage presets"
|
|
||||||
>
|
|
||||||
<FolderOpen className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
{masterEffectChain.effects.length > 0 && (
|
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon-sm"
|
size="icon-sm"
|
||||||
onClick={onClearMasterChain}
|
onClick={() => setPresetDialogOpen(true)}
|
||||||
title="Clear all effects"
|
title="Manage presets"
|
||||||
>
|
>
|
||||||
<Trash2 className="h-4 w-4 text-destructive" />
|
<FolderOpen className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
{masterEffectChain.effects.length > 0 && (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon-sm"
|
||||||
|
onClick={onClearMasterChain}
|
||||||
|
title="Clear all effects"
|
||||||
|
>
|
||||||
|
<Trash2 className="h-4 w-4 text-destructive" />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<EffectRack
|
<EffectRack
|
||||||
chain={masterEffectChain}
|
chain={masterEffectChain}
|
||||||
onToggleEffect={onToggleMasterEffect}
|
onToggleEffect={onToggleMasterEffect}
|
||||||
onRemoveEffect={onRemoveMasterEffect}
|
onRemoveEffect={onRemoveMasterEffect}
|
||||||
onReorderEffects={onReorderMasterEffects}
|
onReorderEffects={onReorderMasterEffects}
|
||||||
/>
|
/>
|
||||||
<PresetManager
|
<PresetManager
|
||||||
open={presetDialogOpen}
|
open={presetDialogOpen}
|
||||||
onClose={() => setPresetDialogOpen(false)}
|
onClose={() => setPresetDialogOpen(false)}
|
||||||
currentChain={masterEffectChain}
|
currentChain={masterEffectChain}
|
||||||
presets={masterEffectPresets}
|
presets={masterEffectPresets}
|
||||||
onSavePreset={onSaveMasterPreset}
|
onSavePreset={onSaveMasterPreset}
|
||||||
onLoadPreset={onLoadMasterPreset}
|
onLoadPreset={onLoadMasterPreset}
|
||||||
onDeletePreset={onDeleteMasterPreset}
|
onDeletePreset={onDeleteMasterPreset}
|
||||||
onExportPreset={() => {}}
|
onExportPreset={() => {}}
|
||||||
onImportPreset={(preset) => onSaveMasterPreset(preset)}
|
onImportPreset={(preset) => onSaveMasterPreset(preset)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user