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

@@ -2,8 +2,7 @@
import * as React from 'react';
import { cn } from '@/lib/utils/cn';
import type { AutomationLane as AutomationLaneType, AutomationPoint as AutomationPointType, AutomationMode } from '@/types/automation';
import { AutomationHeader } from './AutomationHeader';
import type { AutomationLane as AutomationLaneType, AutomationPoint as AutomationPointType } from '@/types/automation';
import { AutomationPoint } from './AutomationPoint';
export interface AutomationLaneProps {
@@ -16,10 +15,6 @@ export interface AutomationLaneProps {
onUpdatePoint?: (pointId: string, updates: Partial<AutomationPointType>) => void;
onRemovePoint?: (pointId: string) => void;
className?: string;
// Parameter selection
availableParameters?: Array<{ id: string; name: string }>;
selectedParameterId?: string;
onParameterChange?: (parameterId: string) => void;
}
export function AutomationLane({
@@ -32,9 +27,6 @@ export function AutomationLane({
onUpdatePoint,
onRemovePoint,
className,
availableParameters,
selectedParameterId,
onParameterChange,
}: AutomationLaneProps) {
const canvasRef = React.useRef<HTMLCanvasElement>(null);
const containerRef = React.useRef<HTMLDivElement>(null);
@@ -294,54 +286,32 @@ export function AutomationLane({
if (!lane.visible) return null;
return (
<div className={cn('flex flex-col', className)} style={{ height: lane.height + 30 }}>
{/* Header */}
<AutomationHeader
parameterName={lane.parameterName}
currentValue={getCurrentValue()}
visible={lane.visible}
mode={lane.mode}
color={lane.color}
onToggleVisible={() => onUpdateLane?.({ visible: !lane.visible })}
onModeChange={(mode: AutomationMode) => onUpdateLane?.({ mode })}
onHeightChange={(delta) => {
const newHeight = Math.max(60, Math.min(180, lane.height + delta));
onUpdateLane?.({ height: newHeight });
}}
formatter={lane.valueRange.formatter}
availableParameters={availableParameters}
selectedParameterId={selectedParameterId}
onParameterChange={onParameterChange}
<div
ref={containerRef}
className={cn('relative bg-background/30 overflow-hidden cursor-crosshair', className)}
style={{ height: lane.height }}
>
<canvas
ref={canvasRef}
className="absolute inset-0 w-full h-full"
onClick={handleCanvasClick}
/>
{/* Lane canvas area */}
<div
ref={containerRef}
className="relative flex-1 bg-background/30 overflow-hidden cursor-crosshair"
style={{ height: lane.height }}
>
<canvas
ref={canvasRef}
className="absolute inset-0 w-full h-full"
onClick={handleCanvasClick}
{/* Automation points */}
{lane.points.map((point) => (
<AutomationPoint
key={point.id}
point={point}
x={timeToX(point.time)}
y={valueToY(point.value)}
isSelected={selectedPointId === point.id}
onDragStart={handlePointDragStart}
onDrag={handlePointDrag}
onDragEnd={handlePointDragEnd}
onClick={handlePointClick}
onDoubleClick={handlePointDoubleClick}
/>
{/* Automation points */}
{lane.points.map((point) => (
<AutomationPoint
key={point.id}
point={point}
x={timeToX(point.time)}
y={valueToY(point.value)}
isSelected={selectedPointId === point.id}
onDragStart={handlePointDragStart}
onDrag={handlePointDrag}
onDragEnd={handlePointDragEnd}
onClick={handlePointClick}
onDoubleClick={handlePointDoubleClick}
/>
))}
</div>
))}
</div>
);
}

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