feat: streamline track and master controls layout consistency

- Streamlined track controls and master controls to same width (240px)
- Fixed track controls container to use full width of parent column
- Matched TrackControls card structure with MasterControls (gap-3, no w-full/h-full)
- Updated outer container padding from p-2 to p-4 with gap-4
- Adjusted track controls wrapper to center content instead of stretching
- Added max-width constraint to PlaybackControls to prevent width changes
- Centered transport control buttons in footer

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-11-19 16:32:49 +01:00
parent 854e64b4ec
commit 5d9e02fe95
9 changed files with 457 additions and 214 deletions

View File

@@ -111,12 +111,7 @@ export function Track({
const canvasRef = React.useRef<HTMLCanvasElement>(null);
const containerRef = React.useRef<HTMLDivElement>(null);
const fileInputRef = React.useRef<HTMLInputElement>(null);
const [isEditingName, setIsEditingName] = React.useState(false);
const [nameInput, setNameInput] = React.useState(
String(track.name || "Untitled Track"),
);
const [themeKey, setThemeKey] = React.useState(0);
const inputRef = React.useRef<HTMLInputElement>(null);
const [isResizing, setIsResizing] = React.useState(false);
const resizeStartRef = React.useRef({ y: 0, height: 0 });
const [effectBrowserOpen, setEffectBrowserOpen] = React.useState(false);
@@ -286,36 +281,6 @@ export function Track({
onUpdateTrack,
]);
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]);
// Listen for theme changes
React.useEffect(() => {
const observer = new MutationObserver(() => {
@@ -686,7 +651,7 @@ export function Track({
<>
<div
className={cn(
"w-48 flex-shrink-0 border-b border-r-4 p-2 flex flex-col gap-2 min-h-0 transition-all duration-200 cursor-pointer border-border",
"w-full flex-shrink-0 border-b border-r-4 p-4 flex flex-col gap-4 min-h-0 transition-all duration-200 cursor-pointer border-border",
isSelected
? "bg-primary/10 border-r-primary"
: "bg-card border-r-transparent hover:bg-accent/30",
@@ -697,76 +662,38 @@ export function Track({
if (onSelect) onSelect();
}}
>
{/* Track Name Row - Integrated collapse (DAW style) */}
<div
className={cn(
"group flex items-center gap-1.5 px-1 py-0.5 rounded cursor-pointer transition-colors",
isSelected ? "bg-primary/10" : "hover:bg-accent/50",
)}
onClick={(e) => {
if (!isEditingName) {
{/* Collapsed Header */}
{track.collapsed && (
<div
className={cn(
"group flex items-center gap-1.5 px-2 py-1 h-full w-full cursor-pointer transition-colors",
isSelected ? "bg-primary/10" : "hover:bg-accent/50",
)}
onClick={(e) => {
e.stopPropagation();
onToggleCollapse();
}
}}
title={track.collapsed ? "Expand track" : "Collapse track"}
>
{/* Small triangle indicator */}
<div
className={cn(
"flex-shrink-0 transition-colors",
isSelected
? "text-primary"
: "text-muted-foreground group-hover:text-foreground",
)}
}}
title="Expand track"
>
{track.collapsed ? (
<ChevronRight className="h-3 w-3" />
) : (
<ChevronDown className="h-3 w-3" />
)}
<ChevronRight className="h-3 w-3 text-muted-foreground flex-shrink-0" />
<div
className="h-4 w-0.5 rounded-full flex-shrink-0"
style={{ backgroundColor: track.color }}
/>
<span className="text-xs font-semibold text-foreground truncate flex-1">
{String(track.name || "Untitled Track")}
</span>
</div>
{/* Color stripe (thicker when selected) */}
<div
className={cn(
"h-5 rounded-full flex-shrink-0 transition-all",
isSelected ? "w-1" : "w-0.5",
)}
style={{ backgroundColor: track.color }}
></div>
<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}
onClick={(e) => e.stopPropagation()}
className="w-full px-1 py-0.5 text-xs font-semibold bg-background border border-border rounded"
/>
) : (
<div
onClick={(e) => {
e.stopPropagation();
handleNameClick();
}}
className="px-1 py-0.5 text-xs font-semibold text-foreground truncate"
title={String(track.name || "Untitled Track")}
>
{String(track.name || "Untitled Track")}
</div>
)}
</div>
</div>
)}
{/* Track Controls - Only show when not collapsed */}
{!track.collapsed && (
<div className="flex-1 flex flex-col items-center justify-between min-h-0 overflow-hidden">
<div className="flex-1 flex flex-col items-center justify-center min-h-0 overflow-hidden">
{/* Integrated Track Controls (Pan + Fader + Buttons) */}
<TrackControls
trackName={track.name}
trackColor={track.color}
collapsed={track.collapsed}
volume={track.volume}
pan={track.pan}
peakLevel={
@@ -785,6 +712,8 @@ export function Track({
showAutomation={track.automation?.showAutomation}
showEffects={track.showEffects}
isRecording={isRecording}
onNameChange={onNameChange}
onToggleCollapse={onToggleCollapse}
onVolumeChange={onVolumeChange}
onPanChange={onPanChange}
onMuteToggle={onToggleMute}
@@ -844,14 +773,14 @@ export function Track({
: "100%",
}}
>
{/* Delete Button - Top Right Overlay */}
{/* Delete Button - Top Right Overlay - Stays fixed when scrolling */}
<button
onClick={(e) => {
e.stopPropagation();
onRemove();
}}
className={cn(
"absolute top-2 right-2 z-20 h-6 w-6 rounded flex items-center justify-center transition-all",
"sticky top-2 right-2 float-right z-20 h-6 w-6 rounded flex items-center justify-center transition-all",
"bg-card/80 hover:bg-destructive/90 text-muted-foreground hover:text-white",
"border border-border/50 hover:border-destructive",
"backdrop-blur-sm shadow-sm hover:shadow-md",
@@ -875,11 +804,12 @@ export function Track({
) : (
!track.collapsed && (
<>
{/* Empty state - clickable area for upload with drag & drop */}
<div
className={cn(
"absolute inset-0 flex flex-col items-center justify-center text-sm text-muted-foreground hover:text-foreground transition-colors cursor-pointer group",
"absolute inset-0 w-full h-full transition-colors cursor-pointer",
isDragging
? "bg-primary/20 text-primary border-2 border-primary border-dashed"
? "bg-primary/20 border-2 border-primary border-dashed"
: "hover:bg-accent/50",
)}
onClick={(e) => {
@@ -889,15 +819,7 @@ export function Track({
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onDrop={handleDrop}
>
<Upload className="h-6 w-6 mb-2 opacity-50 group-hover:opacity-100" />
<p>
{isDragging
? "Drop audio file here"
: "Click to load audio file"}
</p>
<p className="text-xs opacity-75 mt-1">or drag & drop</p>
</div>
/>
<input
ref={fileInputRef}
type="file"
@@ -909,6 +831,16 @@ export function Track({
)
)}
</div>
{/* Import Dialog - Also needed in waveform-only mode */}
<ImportDialog
open={showImportDialog}
onClose={handleImportCancel}
onImport={handleImport}
fileName={pendingFile?.name}
sampleRate={fileMetadata.sampleRate}
channels={fileMetadata.channels}
/>
</div>
);
}