- 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>
144 lines
4.2 KiB
TypeScript
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>
|
|
);
|
|
}
|