work in progress implementation of: - mixer with channel strips, faders, pan knobs, level meters - timeline with ruler, playhead, sections, keyframe tracks - pattern and progression pickers for drums/chords - automation lanes and mute tracks - loop bracket for loop region selection - export modal placeholder known issues: - drum pattern changes don't update audio engine - timeline keyframes not connected to scheduler - some UI bugs remain this is a checkpoint commit for further iteration
92 lines
2.6 KiB
TypeScript
92 lines
2.6 KiB
TypeScript
'use client';
|
|
|
|
import { Card } from '@/components/ui/card';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Shuffle } from 'lucide-react';
|
|
import { cn } from '@/lib/utils';
|
|
import { chordProgressions } from '@/lib/audio/patterns';
|
|
|
|
interface ProgressionPickerProps {
|
|
selectedIndex: number | null;
|
|
onSelect: (index: number) => void;
|
|
onRandom: () => void;
|
|
onClose: () => void;
|
|
className?: string;
|
|
}
|
|
|
|
function chordToName(notes: string[]): string {
|
|
if (notes.length === 0) return '';
|
|
|
|
const noteMap: Record<string, string> = {
|
|
'C': 'C', 'D': 'D', 'E': 'E', 'F': 'F', 'G': 'G', 'A': 'A', 'B': 'B',
|
|
};
|
|
|
|
const root = notes[0].replace(/[0-9]/g, '');
|
|
const rootName = root.replace('#', '♯').replace('b', '♭');
|
|
|
|
if (notes.some((n) => n.includes('b') || n.includes('Eb') || n.includes('Bb'))) {
|
|
return `${rootName}m7`;
|
|
}
|
|
|
|
return `${rootName}maj7`;
|
|
}
|
|
|
|
export function ProgressionPicker({
|
|
selectedIndex,
|
|
onSelect,
|
|
onRandom,
|
|
onClose,
|
|
className,
|
|
}: ProgressionPickerProps) {
|
|
return (
|
|
<Card
|
|
className={cn(
|
|
'absolute z-50 p-3 w-64 bg-card border border-border shadow-xl',
|
|
className
|
|
)}
|
|
onClick={(e) => e.stopPropagation()}
|
|
>
|
|
<div className="flex items-center justify-between mb-2">
|
|
<span className="text-xs font-medium text-muted-foreground uppercase">
|
|
Chord Progressions
|
|
</span>
|
|
<Button variant="ghost" size="sm" className="h-6 px-2" onClick={onRandom}>
|
|
<Shuffle className="h-3 w-3 mr-1" />
|
|
Random
|
|
</Button>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
{chordProgressions.map((prog, i) => (
|
|
<button
|
|
key={i}
|
|
className={cn(
|
|
'w-full p-2 rounded-md border transition-colors text-left',
|
|
selectedIndex === i
|
|
? 'border-primary bg-primary/10'
|
|
: 'border-border/50 hover:border-border hover:bg-muted/30'
|
|
)}
|
|
onClick={() => onSelect(i)}
|
|
>
|
|
<span className="text-xs font-medium block mb-1">{prog.name}</span>
|
|
<div className="flex gap-1 text-[10px] text-muted-foreground font-mono">
|
|
{prog.chords.map((chord, j) => (
|
|
<span
|
|
key={j}
|
|
className="px-1.5 py-0.5 bg-muted/50 rounded"
|
|
>
|
|
{chordToName(chord)}
|
|
</span>
|
|
))}
|
|
</div>
|
|
</button>
|
|
))}
|
|
</div>
|
|
|
|
<Button variant="outline" size="sm" className="w-full mt-2" onClick={onClose}>
|
|
Close
|
|
</Button>
|
|
</Card>
|
|
);
|
|
}
|