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>
223 lines
8.1 KiB
TypeScript
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);
|
|
}
|
|
}}
|
|
/>
|
|
</>
|
|
);
|
|
}
|