Files
audio-ui/components/tracks/TrackExtensions.tsx
Sebastian Krüger b57ac5912a 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>
2025-11-19 11:53:38 +01:00

223 lines
8.1 KiB
TypeScript

'use client';
import * as React from 'react';
import { Plus, ChevronDown, ChevronRight } from 'lucide-react';
import type { Track as TrackType } from '@/types/track';
import { Button } from '@/components/ui/Button';
import { cn } from '@/lib/utils/cn';
import { EffectDevice } from '@/components/effects/EffectDevice';
import { EffectBrowser } from '@/components/effects/EffectBrowser';
import type { EffectType } from '@/lib/audio/effects/chain';
export interface TrackExtensionsProps {
track: TrackType;
onUpdateTrack: (trackId: string, updates: Partial<TrackType>) => void;
onToggleEffect?: (effectId: string) => void;
onRemoveEffect?: (effectId: string) => void;
onUpdateEffect?: (effectId: string, parameters: any) => void;
onAddEffect?: (effectType: EffectType) => void;
asOverlay?: boolean; // When true, renders as full overlay without header
}
export function TrackExtensions({
track,
onUpdateTrack,
onToggleEffect,
onRemoveEffect,
onUpdateEffect,
onAddEffect,
asOverlay = false,
}: TrackExtensionsProps) {
const [effectBrowserOpen, setEffectBrowserOpen] = React.useState(false);
// Don't render if track is collapsed (unless it's an overlay, which handles its own visibility)
if (!asOverlay && track.collapsed) {
return null;
}
// Overlay mode: render full-screen effect rack
if (asOverlay) {
return (
<>
<div className="flex flex-col h-full bg-card/95 rounded-lg border border-border shadow-2xl">
{/* Header with close button */}
<div className="flex items-center justify-between px-4 py-3 border-b border-border bg-muted/50">
<div className="flex items-center gap-2">
<span className="text-sm font-medium">Effects</span>
<span className="text-xs text-muted-foreground">
({track.effectChain.effects.length})
</span>
</div>
<div className="flex items-center gap-2">
<Button
variant="ghost"
size="icon-sm"
onClick={() => setEffectBrowserOpen(true)}
title="Add effect"
>
<Plus className="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="icon-sm"
onClick={() => onUpdateTrack(track.id, { showEffects: false })}
title="Close effects"
>
<ChevronDown className="h-4 w-4" />
</Button>
</div>
</div>
{/* Effects rack */}
<div className="flex-1 overflow-x-auto custom-scrollbar p-4">
<div className="flex h-full gap-4">
{track.effectChain.effects.length === 0 ? (
<div className="flex flex-col items-center justify-center w-full text-center gap-3">
<Sparkles className="h-12 w-12 text-muted-foreground/30" />
<div>
<p className="text-sm text-muted-foreground mb-1">No effects yet</p>
<p className="text-xs text-muted-foreground/70">
Click + to add an effect
</p>
</div>
</div>
) : (
track.effectChain.effects.map((effect) => (
<EffectDevice
key={effect.id}
effect={effect}
onToggleEnabled={() => 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 },
});
}}
/>
))
)}
</div>
</div>
</div>
{/* Effect Browser Dialog */}
<EffectBrowser
open={effectBrowserOpen}
onClose={() => setEffectBrowserOpen(false)}
onSelectEffect={(effectType) => {
if (onAddEffect) {
onAddEffect(effectType);
}
}}
/>
</>
);
}
// Original inline mode
return (
<>
{/* Effects Section (Collapsible, Full Width) */}
<div className="bg-muted/50 border-b border-border/50">
{/* Effects Header - clickable to toggle */}
<div
className="flex items-center gap-2 px-3 py-1.5 cursor-pointer hover:bg-accent/30 transition-colors"
onClick={() => {
onUpdateTrack(track.id, {
showEffects: !track.showEffects,
});
}}
>
{track.showEffects ? (
<ChevronDown className="h-3.5 w-3.5 text-muted-foreground flex-shrink-0" />
) : (
<ChevronRight className="h-3.5 w-3.5 text-muted-foreground flex-shrink-0" />
)}
{/* Show mini effect chain when collapsed */}
{!track.showEffects && track.effectChain.effects.length > 0 ? (
<div className="flex-1 flex items-center gap-1 overflow-x-auto custom-scrollbar">
{track.effectChain.effects.map((effect) => (
<div
key={effect.id}
className={cn(
'px-2 py-0.5 rounded text-[10px] font-medium flex-shrink-0',
effect.enabled
? 'bg-primary/20 text-primary border border-primary/30'
: 'bg-muted/30 text-muted-foreground border border-border'
)}
>
{effect.name}
</div>
))}
</div>
) : (
<span className="text-xs font-medium text-muted-foreground">
Devices ({track.effectChain.effects.length})
</span>
)}
<Button
variant="ghost"
size="icon-sm"
onClick={(e) => {
e.stopPropagation();
setEffectBrowserOpen(true);
}}
title="Add effect"
className="h-5 w-5 flex-shrink-0"
>
<Plus className="h-3 w-3" />
</Button>
</div>
{/* Horizontal scrolling device rack - expanded state */}
{track.showEffects && (
<div className="h-48 overflow-x-auto custom-scrollbar bg-muted/70 p-3">
<div className="flex h-full gap-3">
{track.effectChain.effects.length === 0 ? (
<div className="text-xs text-muted-foreground text-center py-8 w-full">
No devices. Click + to add an effect.
</div>
) : (
track.effectChain.effects.map((effect) => (
<EffectDevice
key={effect.id}
effect={effect}
onToggleEnabled={() => 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 },
});
}}
/>
))
)}
</div>
</div>
)}
</div>
{/* Effect Browser Dialog */}
<EffectBrowser
open={effectBrowserOpen}
onClose={() => setEffectBrowserOpen(false)}
onSelectEffect={(effectType) => {
if (onAddEffect) {
onAddEffect(effectType);
}
}}
/>
</>
);
}