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:
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user