From d2709b8fc23675f9e77da7f4a1b3b9cdec02e9cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Kr=C3=BCger?= Date: Tue, 18 Nov 2025 18:44:06 +0100 Subject: [PATCH] refactor: move effects panel from global to per-track MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Added showEffects property to Track type - Added "E" button with Sparkles icon to toggle per-track effects - Effects panel now appears below each track when toggled - Removed global EffectsPanel from AudioEditor - Updated useMultiTrack to persist showEffects state - Streamlined workflow: both automation and effects are now per-track This aligns the UX with professional DAWs like Ableton Live, where effects and automation are track-scoped rather than global. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- components/editor/AudioEditor.tsx | 21 -------- components/tracks/Track.tsx | 79 ++++++++++++++++++++++++++++++- lib/audio/track-utils.ts | 1 + lib/hooks/useMultiTrack.ts | 2 + types/track.ts | 1 + 5 files changed, 82 insertions(+), 22 deletions(-) diff --git a/components/editor/AudioEditor.tsx b/components/editor/AudioEditor.tsx index 573661a..bc39d61 100644 --- a/components/editor/AudioEditor.tsx +++ b/components/editor/AudioEditor.tsx @@ -14,7 +14,6 @@ import { useEffectChain } from '@/lib/hooks/useEffectChain'; import { useToast } from '@/components/ui/Toast'; import { TrackList } from '@/components/tracks/TrackList'; import { ImportTrackDialog } from '@/components/tracks/ImportTrackDialog'; -import { EffectsPanel } from '@/components/effects/EffectsPanel'; import { formatDuration } from '@/lib/audio/decoder'; import { useHistory } from '@/lib/hooks/useHistory'; import { useRecording } from '@/lib/hooks/useRecording'; @@ -40,8 +39,6 @@ export function AudioEditor() { const [punchOutTime, setPunchOutTime] = React.useState(0); const [overdubEnabled, setOverdubEnabled] = React.useState(false); const [settingsDialogOpen, setSettingsDialogOpen] = React.useState(false); - const [effectsPanelHeight, setEffectsPanelHeight] = React.useState(300); - const [effectsPanelVisible, setEffectsPanelVisible] = React.useState(false); const { addToast } = useToast(); @@ -816,24 +813,6 @@ export function AudioEditor() { trackLevels={trackLevels} /> - - {/* Effects Panel - Global Folding State, Collapsed when no track */} - { - if (selectedTrack) { - setEffectsPanelVisible(!effectsPanelVisible); - } - }} - onResizeHeight={setEffectsPanelHeight} - onAddEffect={handleAddEffect} - onToggleEffect={handleToggleEffect} - onRemoveEffect={handleRemoveEffect} - onUpdateEffect={handleUpdateEffect} - onToggleEffectExpanded={handleToggleEffectExpanded} - /> diff --git a/components/tracks/Track.tsx b/components/tracks/Track.tsx index 78694f5..6291bef 100644 --- a/components/tracks/Track.tsx +++ b/components/tracks/Track.tsx @@ -1,7 +1,7 @@ 'use client'; import * as React from 'react'; -import { Volume2, VolumeX, Headphones, Trash2, ChevronDown, ChevronRight, UnfoldHorizontal, Upload, Mic, Gauge, Circle } from 'lucide-react'; +import { Volume2, VolumeX, Headphones, Trash2, ChevronDown, ChevronRight, UnfoldHorizontal, Upload, Mic, Gauge, Circle, Sparkles } from 'lucide-react'; import type { Track as TrackType } from '@/types/track'; import { COLLAPSED_TRACK_HEIGHT, MIN_TRACK_HEIGHT, MAX_TRACK_HEIGHT } from '@/types/track'; import { Button } from '@/components/ui/Button'; @@ -13,6 +13,8 @@ import { CircularKnob } from '@/components/ui/CircularKnob'; import { AutomationLane } from '@/components/automation/AutomationLane'; import type { AutomationLane as AutomationLaneType, AutomationPoint as AutomationPointType } from '@/types/automation'; import { createAutomationPoint } from '@/lib/audio/automation/utils'; +import { EffectDevice } from '@/components/effects/EffectDevice'; +import { EffectBrowser } from '@/components/effects/EffectBrowser'; export interface TrackProps { track: TrackType; @@ -78,6 +80,7 @@ export function Track({ const inputRef = React.useRef(null); const [isResizing, setIsResizing] = React.useState(false); const resizeStartRef = React.useRef({ y: 0, height: 0 }); + const [effectBrowserOpen, setEffectBrowserOpen] = React.useState(false); // Selection state const [isSelecting, setIsSelecting] = React.useState(false); @@ -617,6 +620,24 @@ export function Track({ > A + + {/* Effects Toggle */} + @@ -751,6 +772,62 @@ export function Track({ )} + {/* Per-Track Effects Panel */} + {!track.collapsed && track.showEffects && ( +
+
+ + {track.name} - Effects + + +
+ + {track.effectChain.effects.length === 0 ? ( +
+ No effects on this track +
+ ) : ( +
+ {track.effectChain.effects.map((effect) => ( + onToggleEffect?.(effect.id)} + onRemove={() => onRemoveEffect?.(effect.id)} + onUpdateParameters={(params) => onUpdateEffect?.(effect.id, params)} + onToggleExpanded={() => { + const updatedEffects = track.effectChain.effects.map((e) => + e.id === effect.id ? { ...e, expanded: !e.expanded } : e + ); + onUpdateTrack(track.id, { + effectChain: { ...track.effectChain, effects: updatedEffects }, + }); + }} + /> + ))} +
+ )} + + {/* Effect Browser Dialog */} + setEffectBrowserOpen(false)} + onSelectEffect={(effectType) => { + onAddEffect?.(effectType); + setEffectBrowserOpen(false); + }} + /> +
+ )} + {/* Track Height Resize Handle */} {!track.collapsed && (