Files
audio-ui/components/tracks/TrackHeader.tsx
Sebastian Krüger 53d436a174 fix: ensure track name is always converted to string in TrackHeader
Convert track.name to string in all state initializations and updates
to prevent '[object Object]' rendering issues.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-17 22:39:43 +01:00

181 lines
5.2 KiB
TypeScript

'use client';
import * as React from 'react';
import { Volume2, VolumeX, Headphones, Mic, X, ChevronDown, ChevronRight } from 'lucide-react';
import { Button } from '@/components/ui/Button';
import { Slider } from '@/components/ui/Slider';
import type { Track } from '@/types/track';
import { cn } from '@/lib/utils/cn';
export interface TrackHeaderProps {
track: Track;
onToggleMute: () => void;
onToggleSolo: () => void;
onToggleCollapse: () => void;
onVolumeChange: (volume: number) => void;
onPanChange: (pan: number) => void;
onRemove: () => void;
onNameChange: (name: string) => void;
}
export function TrackHeader({
track,
onToggleMute,
onToggleSolo,
onToggleCollapse,
onVolumeChange,
onPanChange,
onRemove,
onNameChange,
}: TrackHeaderProps) {
const [isEditingName, setIsEditingName] = React.useState(false);
const [nameInput, setNameInput] = React.useState(String(track.name || 'Untitled Track'));
const inputRef = React.useRef<HTMLInputElement>(null);
const handleNameClick = () => {
setIsEditingName(true);
setNameInput(String(track.name || 'Untitled Track'));
};
const handleNameBlur = () => {
setIsEditingName(false);
if (nameInput.trim()) {
onNameChange(nameInput.trim());
} else {
setNameInput(String(track.name || 'Untitled Track'));
}
};
const handleNameKeyDown = (e: React.KeyboardEvent) => {
if (e.key === 'Enter') {
inputRef.current?.blur();
} else if (e.key === 'Escape') {
setNameInput(String(track.name || 'Untitled Track'));
setIsEditingName(false);
}
};
React.useEffect(() => {
if (isEditingName && inputRef.current) {
inputRef.current.focus();
inputRef.current.select();
}
}, [isEditingName]);
return (
<div className="flex items-center gap-2 px-3 py-2 border-b border-border bg-card">
{/* Collapse Toggle */}
<Button
variant="ghost"
size="icon-sm"
onClick={onToggleCollapse}
title={track.collapsed ? 'Expand track' : 'Collapse track'}
>
{track.collapsed ? (
<ChevronRight className="h-4 w-4" />
) : (
<ChevronDown className="h-4 w-4" />
)}
</Button>
{/* Track Color Indicator */}
<div
className="w-1 h-8 rounded-full"
style={{ backgroundColor: track.color }}
/>
{/* Track Name */}
<div className="flex-1 min-w-0">
{isEditingName ? (
<input
ref={inputRef}
type="text"
value={nameInput}
onChange={(e) => setNameInput(e.target.value)}
onBlur={handleNameBlur}
onKeyDown={handleNameKeyDown}
className="w-full px-2 py-1 text-sm font-medium bg-background border border-border rounded"
/>
) : (
<div
onClick={handleNameClick}
className="px-2 py-1 text-sm font-medium text-foreground truncate cursor-pointer hover:bg-accent rounded"
title={String(track.name || 'Untitled Track')}
>
{String(track.name || 'Untitled Track')}
</div>
)}
</div>
{/* Solo Button */}
<Button
variant={track.solo ? 'secondary' : 'ghost'}
size="icon-sm"
onClick={onToggleSolo}
title="Solo track"
className={cn(track.solo && 'bg-yellow-500/20 hover:bg-yellow-500/30')}
>
<Headphones className={cn('h-4 w-4', track.solo && 'text-yellow-500')} />
</Button>
{/* Mute Button */}
<Button
variant={track.mute ? 'secondary' : 'ghost'}
size="icon-sm"
onClick={onToggleMute}
title="Mute track"
className={cn(track.mute && 'bg-red-500/20 hover:bg-red-500/30')}
>
{track.mute ? (
<VolumeX className="h-4 w-4 text-red-500" />
) : (
<Volume2 className="h-4 w-4" />
)}
</Button>
{/* Volume Slider */}
<div className="flex items-center gap-2 w-24">
<Slider
value={[track.volume]}
onValueChange={([value]) => onVolumeChange(value)}
min={0}
max={1}
step={0.01}
className="flex-1"
title={`Volume: ${Math.round(track.volume * 100)}%`}
/>
<span className="text-xs text-muted-foreground w-8 text-right">
{Math.round(track.volume * 100)}
</span>
</div>
{/* Pan Knob (simplified as slider) */}
<div className="flex items-center gap-2 w-20">
<Slider
value={[track.pan]}
onValueChange={([value]) => onPanChange(value)}
min={-1}
max={1}
step={0.01}
className="flex-1"
title={`Pan: ${track.pan === 0 ? 'C' : track.pan < 0 ? `L${Math.abs(Math.round(track.pan * 100))}` : `R${Math.round(track.pan * 100)}`}`}
/>
<span className="text-xs text-muted-foreground w-6 text-right">
{track.pan === 0 ? 'C' : track.pan < 0 ? `L` : 'R'}
</span>
</div>
{/* Remove Button */}
<Button
variant="ghost"
size="icon-sm"
onClick={onRemove}
title="Remove track"
className="text-destructive hover:text-destructive hover:bg-destructive/10"
>
<X className="h-4 w-4" />
</Button>
</div>
);
}