Avery Felts 26d66f329c Add song structure generation and fix volume controls
- Fix volume sliders by tracking currentVolume separately from mute state
- Add song structure system with intro, verse, bridge, chorus, outro sections
- Implement automatic section transitions during playback based on duration
- Add 30-second loading bar with progress animation during beat generation
- Update instrument box colors (grey drums, red bass, green piano, baby blue brass)
- Add custom trumpet SVG icon for brass section
- Set duration slider max to 3 minutes
- Fix TypeScript type errors in audio engine classes

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-20 23:30:33 -07:00

144 lines
4.2 KiB
TypeScript

'use client';
import { Toggle } from '@/components/ui/toggle';
import { Slider } from '@/components/ui/slider';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import { Volume2, VolumeX } from 'lucide-react';
import { ReactNode } from 'react';
interface InstrumentOption {
readonly value: string;
readonly label: string;
}
interface LayerBoxProps {
title: string;
icon: ReactNode;
volume: number;
muted: boolean;
instrument: string;
instrumentOptions: readonly InstrumentOption[];
onVolumeChange: (volume: number) => void;
onToggleMute: () => void;
onInstrumentChange: (instrument: string) => void;
accentColor: 'grey' | 'red' | 'green' | 'babyblue' | 'pink' | 'purple';
}
const accentStyles = {
grey: {
border: 'border-slate-400/30 hover:border-slate-400/50',
bg: 'bg-slate-300/10',
icon: 'text-slate-400',
slider: '[&_[data-slot=slider-range]]:bg-slate-400 [&_[data-slot=slider-thumb]]:border-slate-400',
},
red: {
border: 'border-rose-400/30 hover:border-rose-400/50',
bg: 'bg-rose-400/8',
icon: 'text-rose-400',
slider: '[&_[data-slot=slider-range]]:bg-rose-400 [&_[data-slot=slider-thumb]]:border-rose-400',
},
green: {
border: 'border-emerald-400/30 hover:border-emerald-400/50',
bg: 'bg-gradient-to-r from-emerald-400/10 to-white/5',
icon: 'text-emerald-400',
slider: '[&_[data-slot=slider-range]]:bg-emerald-400 [&_[data-slot=slider-thumb]]:border-emerald-400',
},
babyblue: {
border: 'border-sky-300/30 hover:border-sky-300/50',
bg: 'bg-sky-300/8',
icon: 'text-sky-300',
slider: '[&_[data-slot=slider-range]]:bg-sky-300 [&_[data-slot=slider-thumb]]:border-sky-300',
},
pink: {
border: 'border-lofi-pink/30 hover:border-lofi-pink/50',
bg: 'bg-lofi-pink/5',
icon: 'text-lofi-pink',
slider: '[&_[data-slot=slider-range]]:bg-lofi-pink [&_[data-slot=slider-thumb]]:border-lofi-pink',
},
purple: {
border: 'border-lofi-purple/30 hover:border-lofi-purple/50',
bg: 'bg-lofi-purple/5',
icon: 'text-lofi-purple',
slider: '[&_[data-slot=slider-range]]:bg-lofi-purple [&_[data-slot=slider-thumb]]:border-lofi-purple',
},
};
export function LayerBox({
title,
icon,
volume,
muted,
instrument,
instrumentOptions,
onVolumeChange,
onToggleMute,
onInstrumentChange,
accentColor,
}: LayerBoxProps) {
const styles = accentStyles[accentColor];
return (
<div
className={`
rounded-xl border-2 p-4 transition-all duration-200
${styles.border} ${styles.bg}
`}
>
<div className="flex items-center justify-between mb-3">
<div className="flex items-center gap-2">
<span className={styles.icon}>{icon}</span>
<span className="font-medium text-sm">{title}</span>
</div>
<div className="flex items-center gap-2">
<Select value={instrument} onValueChange={onInstrumentChange}>
<SelectTrigger className="h-7 w-28 text-xs bg-secondary/50 border-border/50">
<SelectValue />
</SelectTrigger>
<SelectContent>
{instrumentOptions.map((opt) => (
<SelectItem key={opt.value} value={opt.value} className="text-xs">
{opt.label}
</SelectItem>
))}
</SelectContent>
</Select>
<Toggle
pressed={!muted}
onPressedChange={onToggleMute}
size="sm"
className="h-7 w-7 p-0"
aria-label={`Toggle ${title}`}
>
{muted ? (
<VolumeX className="h-3.5 w-3.5 text-muted-foreground" />
) : (
<Volume2 className={`h-3.5 w-3.5 ${styles.icon}`} />
)}
</Toggle>
</div>
</div>
<div className="flex items-center gap-3">
<Slider
value={[volume]}
onValueChange={([v]) => onVolumeChange(v)}
min={0}
max={1}
step={0.01}
className={`flex-1 ${styles.slider}`}
disabled={muted}
/>
<span className="text-xs text-muted-foreground font-mono w-10 text-right">
{Math.round(volume * 100)}%
</span>
</div>
</div>
);
}