refactor: move automation controls to left sidebar, simplify layout

Major automation UX improvements:
- Moved all automation controls to left sidebar (180px, matching track controls)
  - Parameter dropdown selector
  - Automation mode button (R/W/T/L with color coding)
  - Height adjustment buttons (+/-)
- Automation canvas now fills right side (matching waveform width exactly)
- Removed AutomationHeader component (no longer needed)
- Removed eye icon (automation visibility controlled by "A" button on track)

Two-column layout consistency:
- Left: 180px sidebar with all controls
- Right: Flexible canvas area matching waveform width
- Perfect vertical alignment between waveform and automation

Simplified AutomationLane component:
- Now only renders the canvas area with points
- All controls handled in parent Track component
- Cleaner, more maintainable code structure

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-11-18 18:57:51 +01:00
parent 3cc4cb555a
commit a5c5289424
2 changed files with 109 additions and 66 deletions

View File

@@ -1,7 +1,7 @@
'use client';
import * as React from 'react';
import { Volume2, VolumeX, Headphones, Trash2, ChevronDown, ChevronRight, UnfoldHorizontal, Upload, Mic, Gauge, Circle, Sparkles } from 'lucide-react';
import { Volume2, VolumeX, Headphones, Trash2, ChevronDown, ChevronRight, ChevronUp, UnfoldHorizontal, Upload, Mic, Gauge, Circle, Sparkles } from 'lucide-react';
import type { Track as TrackType } from '@/types/track';
import { COLLAPSED_TRACK_HEIGHT, MIN_TRACK_HEIGHT, MAX_TRACK_HEIGHT } from '@/types/track';
import { Button } from '@/components/ui/Button';
@@ -765,26 +765,99 @@ export function Track({
}
}
const modes: Array<{ value: string; label: string; color: string }> = [
{ value: 'read', label: 'R', color: 'text-muted-foreground' },
{ value: 'write', label: 'W', color: 'text-red-500' },
{ value: 'touch', label: 'T', color: 'text-yellow-500' },
{ value: 'latch', label: 'L', color: 'text-orange-500' },
];
const currentModeIndex = modes.findIndex(m => m.value === selectedLane?.mode);
return selectedLane ? (
<div className="flex border-b border-border">
{/* Left: Sidebar spacer to align with track controls */}
<div className="w-[180px] flex-shrink-0 bg-background/30" />
{/* Left: Automation Controls (matching track controls width) */}
<div className="w-[180px] flex-shrink-0 bg-muted/30 border-r border-border/50 p-2 flex flex-col gap-2">
{/* Parameter selector dropdown */}
<select
value={selectedParameterId}
onChange={(e) => {
onUpdateTrack(track.id, {
automation: { ...track.automation, selectedParameterId: e.target.value },
});
}}
className="w-full text-xs font-medium text-foreground bg-background/80 border border-border/30 rounded px-2 py-1 hover:bg-background focus:outline-none focus:ring-1 focus:ring-primary"
>
{availableParameters.map((param) => (
<option key={param.id} value={param.id}>
{param.name}
</option>
))}
</select>
{/* Right: Automation lane matching waveform width */}
<div className="flex-1">
{/* Automation mode cycle button */}
<button
onClick={() => {
const nextIndex = (currentModeIndex + 1) % modes.length;
const updatedLanes = track.automation.lanes.map((l) =>
l.id === selectedLane.id ? { ...l, mode: modes[nextIndex].value as any } : l
);
onUpdateTrack(track.id, {
automation: { ...track.automation, lanes: updatedLanes },
});
}}
className={cn(
'w-full px-2 py-1 text-xs font-bold rounded transition-colors border border-border/30',
'bg-background/50 hover:bg-background',
modes[currentModeIndex]?.color
)}
title={`Mode: ${selectedLane.mode} (click to cycle)`}
>
{modes[currentModeIndex]?.label} - {selectedLane.mode.toUpperCase()}
</button>
{/* Height controls */}
<div className="flex gap-1">
<button
onClick={() => {
const newHeight = Math.max(60, Math.min(180, selectedLane.height + 20));
const updatedLanes = track.automation.lanes.map((l) =>
l.id === selectedLane.id ? { ...l, height: newHeight } : l
);
onUpdateTrack(track.id, {
automation: { ...track.automation, lanes: updatedLanes },
});
}}
className="flex-1 px-2 py-1 text-xs bg-background/50 hover:bg-background border border-border/30 rounded transition-colors"
title="Increase lane height"
>
<ChevronUp className="h-3 w-3 mx-auto" />
</button>
<button
onClick={() => {
const newHeight = Math.max(60, Math.min(180, selectedLane.height - 20));
const updatedLanes = track.automation.lanes.map((l) =>
l.id === selectedLane.id ? { ...l, height: newHeight } : l
);
onUpdateTrack(track.id, {
automation: { ...track.automation, lanes: updatedLanes },
});
}}
className="flex-1 px-2 py-1 text-xs bg-background/50 hover:bg-background border border-border/30 rounded transition-colors"
title="Decrease lane height"
>
<ChevronDown className="h-3 w-3 mx-auto" />
</button>
</div>
</div>
{/* Right: Automation Lane Canvas (matching waveform width) */}
<div className="flex-1 border-l border-border/50">
<AutomationLane
key={selectedLane.id}
lane={selectedLane}
duration={duration}
zoom={zoom}
currentTime={currentTime}
availableParameters={availableParameters}
selectedParameterId={selectedParameterId}
onParameterChange={(parameterId) => {
onUpdateTrack(track.id, {
automation: { ...track.automation, selectedParameterId: parameterId },
});
}}
onUpdateLane={(updates) => {
const updatedLanes = track.automation.lanes.map((l) =>
l.id === selectedLane.id ? { ...l, ...updates } : l