Enhanced visual design: - Improved device rack container with darker background and inner shadow - Device cards now have rounded corners, shadows, and colored indicators - Better visual separation between enabled/disabled effects - Active devices highlighted with accent border Complete automation infrastructure (Phase 9): - Created comprehensive type system for automation lanes and points - Implemented AutomationPoint component with drag-and-drop editing - Implemented AutomationHeader with mode controls (Read/Write/Touch/Latch) - Implemented AutomationLane with canvas-based curve rendering - Integrated automation lanes into Track component below effects - Created automation playback engine with real-time interpolation - Added automation data persistence to localStorage Automation features: - Add/remove automation points by clicking/double-clicking - Drag points to change time and value - Multiple automation modes (Read, Write, Touch, Latch) - Linear and step curve types (bezier planned) - Adjustable lane height (60-180px) - Show/hide automation per lane - Real-time value display at playhead - Color-coded lanes by parameter type - Keyboard delete support (Delete/Backspace) Track type updates: - Added automation field to Track interface - Updated track creation to initialize empty automation - Updated localStorage save/load to include automation data Files created: - components/automation/AutomationPoint.tsx - components/automation/AutomationHeader.tsx - components/automation/AutomationLane.tsx - lib/audio/automation/utils.ts (helper functions) - lib/audio/automation/playback.ts (playback engine) - types/automation.ts (complete type system) Files modified: - components/effects/EffectDevice.tsx (Ableton-style visual improvements) - components/tracks/Track.tsx (automation lanes integration) - types/track.ts (automation field added) - lib/audio/track-utils.ts (automation initialization) - lib/hooks/useMultiTrack.ts (automation persistence) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
116 lines
4.0 KiB
TypeScript
116 lines
4.0 KiB
TypeScript
'use client';
|
|
|
|
import * as React from 'react';
|
|
import { ChevronLeft, ChevronRight, Power, X } from 'lucide-react';
|
|
import { Button } from '@/components/ui/Button';
|
|
import { cn } from '@/lib/utils/cn';
|
|
import type { ChainEffect } from '@/lib/audio/effects/chain';
|
|
import { EffectParameters } from './EffectParameters';
|
|
|
|
export interface EffectDeviceProps {
|
|
effect: ChainEffect;
|
|
onToggleEnabled?: () => void;
|
|
onRemove?: () => void;
|
|
onUpdateParameters?: (parameters: any) => void;
|
|
}
|
|
|
|
export function EffectDevice({
|
|
effect,
|
|
onToggleEnabled,
|
|
onRemove,
|
|
onUpdateParameters,
|
|
}: EffectDeviceProps) {
|
|
const [isExpanded, setIsExpanded] = React.useState(false);
|
|
|
|
return (
|
|
<div
|
|
className={cn(
|
|
'flex-shrink-0 flex flex-col h-full transition-all duration-200 rounded-md overflow-hidden',
|
|
effect.enabled
|
|
? 'bg-card border border-border/50 shadow-md'
|
|
: 'bg-card/40 border border-border/30 shadow-sm opacity-60',
|
|
isExpanded ? 'min-w-96' : 'w-10'
|
|
)}
|
|
>
|
|
{!isExpanded ? (
|
|
/* Collapsed State */
|
|
<>
|
|
{/* Colored top indicator */}
|
|
<div className={cn('h-0.5 w-full', effect.enabled ? 'bg-primary' : 'bg-muted-foreground/20')} />
|
|
|
|
<button
|
|
onClick={() => setIsExpanded(true)}
|
|
className="w-full h-full flex flex-col items-center justify-between py-1 hover:bg-primary/10 transition-colors group"
|
|
title={`Expand ${effect.name}`}
|
|
>
|
|
<ChevronRight className="h-3 w-3 flex-shrink-0 text-muted-foreground group-hover:text-primary transition-colors" />
|
|
<span
|
|
className="flex-1 text-xs font-medium whitespace-nowrap text-muted-foreground group-hover:text-primary transition-colors"
|
|
style={{
|
|
writingMode: 'vertical-rl',
|
|
textOrientation: 'mixed',
|
|
}}
|
|
>
|
|
{effect.name}
|
|
</span>
|
|
<div
|
|
className={cn(
|
|
'w-1.5 h-1.5 rounded-full flex-shrink-0 mb-1',
|
|
effect.enabled ? 'bg-primary shadow-sm shadow-primary/50' : 'bg-muted-foreground/30'
|
|
)}
|
|
title={effect.enabled ? 'Enabled' : 'Disabled'}
|
|
/>
|
|
</button>
|
|
</>
|
|
) : (
|
|
<>
|
|
{/* Colored top indicator */}
|
|
<div className={cn('h-0.5 w-full', effect.enabled ? 'bg-primary' : 'bg-muted-foreground/20')} />
|
|
|
|
{/* Full-Width Header Row */}
|
|
<div className="flex items-center gap-1 px-2 py-1.5 border-b border-border/50 bg-muted/30 flex-shrink-0">
|
|
<Button
|
|
variant="ghost"
|
|
size="icon-sm"
|
|
onClick={() => setIsExpanded(false)}
|
|
title="Collapse device"
|
|
className="h-5 w-5 flex-shrink-0"
|
|
>
|
|
<ChevronLeft className="h-3 w-3" />
|
|
</Button>
|
|
<span className="text-xs font-semibold flex-1 min-w-0 truncate">{effect.name}</span>
|
|
<Button
|
|
variant="ghost"
|
|
size="icon-sm"
|
|
onClick={onToggleEnabled}
|
|
title={effect.enabled ? 'Disable effect' : 'Enable effect'}
|
|
className="h-5 w-5 flex-shrink-0"
|
|
>
|
|
<Power
|
|
className={cn(
|
|
'h-3 w-3',
|
|
effect.enabled ? 'text-primary' : 'text-muted-foreground'
|
|
)}
|
|
/>
|
|
</Button>
|
|
<Button
|
|
variant="ghost"
|
|
size="icon-sm"
|
|
onClick={onRemove}
|
|
title="Remove effect"
|
|
className="h-5 w-5 flex-shrink-0"
|
|
>
|
|
<X className="h-3 w-3 text-destructive" />
|
|
</Button>
|
|
</div>
|
|
|
|
{/* Device Body */}
|
|
<div className="flex-1 min-h-0 overflow-y-auto custom-scrollbar p-3 bg-card/50">
|
|
<EffectParameters effect={effect} onUpdateParameters={onUpdateParameters} />
|
|
</div>
|
|
</>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|