feat: implement overlay architecture for automation lanes and effects

Automation lanes and effects now render as overlays on top of the waveform
instead of below the track, solving both visibility and layout proportion issues.

Changes:
- Wrapped Track waveforms in relative container for overlay positioning
- Automation lanes render with bg-black/60 backdrop-blur overlay
- Effects render with TrackExtensions in overlay mode (asOverlay prop)
- Added overlay-specific rendering with close button and better empty state
- Both overlays use absolute positioning with z-10 for proper stacking
- Eliminated height mismatch between controls and waveform areas

This approach provides better visual integration and eliminates the need
to match heights between the two-column layout.

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-11-19 11:53:38 +01:00
parent d2ed7d6e78
commit b57ac5912a
2 changed files with 283 additions and 186 deletions

View File

@@ -205,197 +205,208 @@ export function TrackList({
<div className="flex flex-col">
{tracks.map((track) => (
<React.Fragment key={track.id}>
{/* Track Waveform Row */}
<Track
track={track}
zoom={zoom}
currentTime={currentTime}
duration={duration}
isSelected={selectedTrackId === track.id}
onSelect={onSelectTrack ? () => onSelectTrack(track.id) : undefined}
onToggleMute={() =>
onUpdateTrack(track.id, { mute: !track.mute })
}
onToggleSolo={() =>
onUpdateTrack(track.id, { solo: !track.solo })
}
onToggleCollapse={() =>
onUpdateTrack(track.id, { collapsed: !track.collapsed })
}
onVolumeChange={(volume) =>
onUpdateTrack(track.id, { volume })
}
onPanChange={(pan) =>
onUpdateTrack(track.id, { pan })
}
onRemove={() => onRemoveTrack(track.id)}
onNameChange={(name) =>
onUpdateTrack(track.id, { name })
}
onUpdateTrack={onUpdateTrack}
onSeek={onSeek}
onLoadAudio={(buffer) =>
onUpdateTrack(track.id, { audioBuffer: buffer })
}
onToggleEffect={(effectId) => {
const updatedChain = {
...track.effectChain,
effects: track.effectChain.effects.map((e) =>
e.id === effectId ? { ...e, enabled: !e.enabled } : e
),
};
onUpdateTrack(track.id, { effectChain: updatedChain });
}}
onRemoveEffect={(effectId) => {
const updatedChain = {
...track.effectChain,
effects: track.effectChain.effects.filter((e) => e.id !== effectId),
};
onUpdateTrack(track.id, { effectChain: updatedChain });
}}
onUpdateEffect={(effectId, parameters) => {
const updatedChain = {
...track.effectChain,
effects: track.effectChain.effects.map((e) =>
e.id === effectId ? { ...e, parameters } : e
),
};
onUpdateTrack(track.id, { effectChain: updatedChain });
}}
onAddEffect={(effectType) => {
const newEffect = createEffect(
effectType,
EFFECT_NAMES[effectType]
);
const updatedChain = {
...track.effectChain,
effects: [...track.effectChain.effects, newEffect],
};
onUpdateTrack(track.id, { effectChain: updatedChain });
}}
onSelectionChange={
onSelectionChange
? (selection) => onSelectionChange(track.id, selection)
: undefined
}
onToggleRecordEnable={
onToggleRecordEnable
? () => onToggleRecordEnable(track.id)
: undefined
}
isRecording={recordingTrackId === track.id}
recordingLevel={recordingTrackId === track.id ? recordingLevel : 0}
playbackLevel={trackLevels[track.id] || 0}
onParameterTouched={onParameterTouched}
isPlaying={isPlaying}
renderWaveformOnly={true}
/>
{/* Track Waveform Row with Overlays */}
<div className="relative">
<Track
track={track}
zoom={zoom}
currentTime={currentTime}
duration={duration}
isSelected={selectedTrackId === track.id}
onSelect={onSelectTrack ? () => onSelectTrack(track.id) : undefined}
onToggleMute={() =>
onUpdateTrack(track.id, { mute: !track.mute })
}
onToggleSolo={() =>
onUpdateTrack(track.id, { solo: !track.solo })
}
onToggleCollapse={() =>
onUpdateTrack(track.id, { collapsed: !track.collapsed })
}
onVolumeChange={(volume) =>
onUpdateTrack(track.id, { volume })
}
onPanChange={(pan) =>
onUpdateTrack(track.id, { pan })
}
onRemove={() => onRemoveTrack(track.id)}
onNameChange={(name) =>
onUpdateTrack(track.id, { name })
}
onUpdateTrack={onUpdateTrack}
onSeek={onSeek}
onLoadAudio={(buffer) =>
onUpdateTrack(track.id, { audioBuffer: buffer })
}
onToggleEffect={(effectId) => {
const updatedChain = {
...track.effectChain,
effects: track.effectChain.effects.map((e) =>
e.id === effectId ? { ...e, enabled: !e.enabled } : e
),
};
onUpdateTrack(track.id, { effectChain: updatedChain });
}}
onRemoveEffect={(effectId) => {
const updatedChain = {
...track.effectChain,
effects: track.effectChain.effects.filter((e) => e.id !== effectId),
};
onUpdateTrack(track.id, { effectChain: updatedChain });
}}
onUpdateEffect={(effectId, parameters) => {
const updatedChain = {
...track.effectChain,
effects: track.effectChain.effects.map((e) =>
e.id === effectId ? { ...e, parameters } : e
),
};
onUpdateTrack(track.id, { effectChain: updatedChain });
}}
onAddEffect={(effectType) => {
const newEffect = createEffect(
effectType,
EFFECT_NAMES[effectType]
);
const updatedChain = {
...track.effectChain,
effects: [...track.effectChain.effects, newEffect],
};
onUpdateTrack(track.id, { effectChain: updatedChain });
}}
onSelectionChange={
onSelectionChange
? (selection) => onSelectionChange(track.id, selection)
: undefined
}
onToggleRecordEnable={
onToggleRecordEnable
? () => onToggleRecordEnable(track.id)
: undefined
}
isRecording={recordingTrackId === track.id}
recordingLevel={recordingTrackId === track.id ? recordingLevel : 0}
playbackLevel={trackLevels[track.id] || 0}
onParameterTouched={onParameterTouched}
isPlaying={isPlaying}
renderWaveformOnly={true}
/>
{/* Automation Lane Section */}
{!track.collapsed && track.automation?.showAutomation && (
<div className="bg-muted/30 border-b border-border">
{track.automation.lanes
.filter((lane) => lane.parameterId === track.automation.selectedParameterId)
.map((lane) => (
<AutomationLane
key={lane.id}
lane={lane}
trackId={track.id}
zoom={zoom}
currentTime={currentTime}
duration={duration}
isPlaying={isPlaying}
onAddPoint={(time, value) => {
const newPoint = createAutomationPoint(time, value);
const updatedLanes = track.automation.lanes.map((l) =>
l.id === lane.id
? { ...l, points: [...l.points, newPoint].sort((a, b) => a.time - b.time) }
: l
);
onUpdateTrack(track.id, {
automation: { ...track.automation, lanes: updatedLanes },
});
{/* Automation Lane Overlay */}
{!track.collapsed && track.automation?.showAutomation && (
<div className="absolute inset-0 bg-black/60 backdrop-blur-sm pointer-events-auto z-10">
<div className="h-full p-4">
{track.automation.lanes
.filter((lane) => lane.parameterId === track.automation.selectedParameterId)
.map((lane) => (
<AutomationLane
key={lane.id}
lane={lane}
trackId={track.id}
zoom={zoom}
currentTime={currentTime}
duration={duration}
isPlaying={isPlaying}
onAddPoint={(time, value) => {
const newPoint = createAutomationPoint(time, value);
const updatedLanes = track.automation.lanes.map((l) =>
l.id === lane.id
? { ...l, points: [...l.points, newPoint].sort((a, b) => a.time - b.time) }
: l
);
onUpdateTrack(track.id, {
automation: { ...track.automation, lanes: updatedLanes },
});
}}
onUpdatePoint={(pointId, updates) => {
const updatedLanes = track.automation.lanes.map((l) =>
l.id === lane.id
? {
...l,
points: l.points.map((p) =>
p.id === pointId ? { ...p, ...updates } : p
),
}
: l
);
onUpdateTrack(track.id, {
automation: { ...track.automation, lanes: updatedLanes },
});
}}
onRemovePoint={(pointId) => {
const updatedLanes = track.automation.lanes.map((l) =>
l.id === lane.id
? { ...l, points: l.points.filter((p) => p.id !== pointId) }
: l
);
onUpdateTrack(track.id, {
automation: { ...track.automation, lanes: updatedLanes },
});
}}
onUpdateLane={(updates) => {
const updatedLanes = track.automation.lanes.map((l) =>
l.id === lane.id ? { ...l, ...updates } : l
);
onUpdateTrack(track.id, {
automation: { ...track.automation, lanes: updatedLanes },
});
}}
onSeek={onSeek}
/>
))}
</div>
</div>
)}
{/* Effects Overlay */}
{!track.collapsed && track.showEffects && (
<div className="absolute inset-0 bg-black/60 backdrop-blur-sm pointer-events-auto z-10">
<div className="h-full p-4">
<TrackExtensions
track={track}
onUpdateTrack={onUpdateTrack}
asOverlay={true}
onToggleEffect={(effectId) => {
const updatedChain = {
...track.effectChain,
effects: track.effectChain.effects.map((e) =>
e.id === effectId ? { ...e, enabled: !e.enabled } : e
),
};
onUpdateTrack(track.id, { effectChain: updatedChain });
}}
onUpdatePoint={(pointId, updates) => {
const updatedLanes = track.automation.lanes.map((l) =>
l.id === lane.id
? {
...l,
points: l.points.map((p) =>
p.id === pointId ? { ...p, ...updates } : p
),
}
: l
);
onUpdateTrack(track.id, {
automation: { ...track.automation, lanes: updatedLanes },
});
onRemoveEffect={(effectId) => {
const updatedChain = {
...track.effectChain,
effects: track.effectChain.effects.filter((e) => e.id !== effectId),
};
onUpdateTrack(track.id, { effectChain: updatedChain });
}}
onRemovePoint={(pointId) => {
const updatedLanes = track.automation.lanes.map((l) =>
l.id === lane.id
? { ...l, points: l.points.filter((p) => p.id !== pointId) }
: l
);
onUpdateTrack(track.id, {
automation: { ...track.automation, lanes: updatedLanes },
});
onUpdateEffect={(effectId, parameters) => {
const updatedChain = {
...track.effectChain,
effects: track.effectChain.effects.map((e) =>
e.id === effectId ? { ...e, parameters } : e
),
};
onUpdateTrack(track.id, { effectChain: updatedChain });
}}
onUpdateLane={(updates) => {
const updatedLanes = track.automation.lanes.map((l) =>
l.id === lane.id ? { ...l, ...updates } : l
onAddEffect={(effectType) => {
const newEffect = createEffect(
effectType,
EFFECT_NAMES[effectType]
);
onUpdateTrack(track.id, {
automation: { ...track.automation, lanes: updatedLanes },
});
const updatedChain = {
...track.effectChain,
effects: [...track.effectChain.effects, newEffect],
};
onUpdateTrack(track.id, { effectChain: updatedChain });
}}
onSeek={onSeek}
/>
))}
</div>
)}
{/* Effects Section */}
<TrackExtensions
track={track}
onUpdateTrack={onUpdateTrack}
onToggleEffect={(effectId) => {
const updatedChain = {
...track.effectChain,
effects: track.effectChain.effects.map((e) =>
e.id === effectId ? { ...e, enabled: !e.enabled } : e
),
};
onUpdateTrack(track.id, { effectChain: updatedChain });
}}
onRemoveEffect={(effectId) => {
const updatedChain = {
...track.effectChain,
effects: track.effectChain.effects.filter((e) => e.id !== effectId),
};
onUpdateTrack(track.id, { effectChain: updatedChain });
}}
onUpdateEffect={(effectId, parameters) => {
const updatedChain = {
...track.effectChain,
effects: track.effectChain.effects.map((e) =>
e.id === effectId ? { ...e, parameters } : e
),
};
onUpdateTrack(track.id, { effectChain: updatedChain });
}}
onAddEffect={(effectType) => {
const newEffect = createEffect(
effectType,
EFFECT_NAMES[effectType]
);
const updatedChain = {
...track.effectChain,
effects: [...track.effectChain.effects, newEffect],
};
onUpdateTrack(track.id, { effectChain: updatedChain });
}}
/>
</div>
</div>
)}
</div>
</React.Fragment>
))}
</div>