feat: Complete Ableton-style track layout redesign

Track height & spacing improvements:
- Increased DEFAULT_TRACK_HEIGHT from 180px to 240px for vertical controls
- Increased MIN_TRACK_HEIGHT from 60px to 120px
- Increased MAX_TRACK_HEIGHT from 300px to 400px
- Added COLLAPSED_TRACK_HEIGHT constant (48px)
- Reduced control panel gap from 8px to 6px for tighter spacing
- Added min-h-0 and overflow-hidden to prevent flex overflow
- Optimized fader container with max-h-[140px] constraint

Clip-style waveform visualization (Ableton-like):
- Wrapped waveform canvas in visible "clip" container
- Added border, rounded corners, and shadow for clip identity
- Added 16px clip header bar showing track name
- Implemented hover state for better interactivity
- Added gradient background from-foreground/5 to-transparent

Track height resize functionality:
- Added draggable bottom-edge resize handle
- Implemented cursor-ns-resize with hover feedback
- Constrain resizing to MIN/MAX height range
- Real-time height updates with smooth visual feedback
- Active state highlighting during resize

Effects section visual integration:
- Changed from solid background to gradient (from-muted/80 to-muted/60)
- Reduced device rack height from 192px to 176px for better proportion
- Improved visual hierarchy and connection to track row

Flexible VerticalFader component:
- Changed from fixed h-32 (128px) to flex-1 layout
- Added min-h-[80px] and max-h-[140px] constraints
- Allows parent container to control actual height
- Maintains readability and proportions at all sizes

CSS enhancements (globals.css):
- Added .track-clip-container utility class
- Added .track-clip-header utility class
- Theme-aware clip styling for light/dark modes
- OKLCH color space for consistent appearance

Visual results:
- Professional DAW appearance matching Ableton Live
- Clear clip/region boundaries for audio editing
- Better proportions for vertical controls (240px tracks)
- Resizable track heights (120-400px range)
- Improved visual hierarchy and organization

Files modified:
- types/track.ts (height constants)
- components/tracks/Track.tsx (layout + clip styling + resize)
- components/ui/VerticalFader.tsx (flexible height)
- app/globals.css (clip styling)

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-11-18 16:41:58 +01:00
parent 9b1eedc379
commit ecf7f060ec
4 changed files with 131 additions and 35 deletions

View File

@@ -354,4 +354,31 @@
.custom-scrollbar::-webkit-scrollbar-thumb:active {
background: color-mix(in oklch, var(--muted-foreground) 70%, transparent);
}
/* Clip/Region styling for Ableton-style appearance */
.track-clip-container {
@apply absolute inset-2 rounded-sm shadow-sm overflow-hidden transition-all duration-150;
background: oklch(0.2 0.01 var(--hue) / 0.3);
border: 1px solid oklch(0.4 0.02 var(--hue) / 0.5);
}
.track-clip-container:hover {
border-color: oklch(0.5 0.03 var(--hue) / 0.7);
}
.track-clip-header {
@apply absolute top-0 left-0 right-0 h-4 pointer-events-none z-10 px-2 flex items-center;
background: linear-gradient(to bottom, rgb(0 0 0 / 0.1), transparent);
}
}
@layer utilities {
[data-theme='light'] .track-clip-container {
background: oklch(0.95 0.01 var(--hue) / 0.3);
border: 1px solid oklch(0.7 0.02 var(--hue) / 0.5);
}
[data-theme='light'] .track-clip-header {
background: linear-gradient(to bottom, rgb(255 255 255 / 0.15), transparent);
}
}

View File

@@ -3,6 +3,7 @@
import * as React from 'react';
import { Volume2, VolumeX, Headphones, Trash2, ChevronDown, ChevronRight, UnfoldHorizontal, Upload, Plus, Mic, Gauge } 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';
import { Slider } from '@/components/ui/Slider';
import { cn } from '@/lib/utils/cn';
@@ -77,6 +78,8 @@ export function Track({
const [showEffects, setShowEffects] = React.useState(false);
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 });
// Selection state
const [isSelecting, setIsSelecting] = React.useState(false);
@@ -393,7 +396,44 @@ export function Track({
}
};
const trackHeight = track.collapsed ? 48 : track.height;
const trackHeight = track.collapsed ? COLLAPSED_TRACK_HEIGHT : track.height;
// Track height resize handlers
const handleResizeStart = React.useCallback(
(e: React.MouseEvent) => {
if (track.collapsed) return;
e.preventDefault();
e.stopPropagation();
setIsResizing(true);
resizeStartRef.current = { y: e.clientY, height: track.height };
},
[track.collapsed, track.height]
);
React.useEffect(() => {
if (!isResizing) return;
const handleMouseMove = (e: MouseEvent) => {
const delta = e.clientY - resizeStartRef.current.y;
const newHeight = Math.max(
MIN_TRACK_HEIGHT,
Math.min(MAX_TRACK_HEIGHT, resizeStartRef.current.height + delta)
);
onUpdateTrack(track.id, { height: newHeight });
};
const handleMouseUp = () => {
setIsResizing(false);
};
window.addEventListener('mousemove', handleMouseMove);
window.addEventListener('mouseup', handleMouseUp);
return () => {
window.removeEventListener('mousemove', handleMouseMove);
window.removeEventListener('mouseup', handleMouseUp);
};
}, [isResizing, onUpdateTrack, track.id]);
return (
<div
@@ -407,7 +447,7 @@ export function Track({
<div className="flex" style={{ height: trackHeight }}>
{/* Left: Track Control Panel (Fixed Width) - Ableton Style */}
<div
className="w-48 flex-shrink-0 bg-card border-r border-border border-b border-border p-2 flex flex-col gap-2"
className="w-48 flex-shrink-0 bg-card border-r border-border border-b border-border p-2 flex flex-col gap-1.5 min-h-0"
onClick={(e) => e.stopPropagation()}
>
{/* Track Name (Full Width) */}
@@ -520,45 +560,59 @@ export function Track({
{/* Track Controls - Only show when not collapsed */}
{!track.collapsed && (
<div className="flex-1 flex flex-col items-center justify-center gap-2 py-2">
<div className="flex-1 flex flex-col items-center justify-between py-1 min-h-0 overflow-hidden">
{/* Pan Knob */}
<CircularKnob
value={track.pan}
onChange={onPanChange}
min={-1}
max={1}
step={0.01}
size={40}
label="PAN"
/>
<div className="flex-shrink-0">
<CircularKnob
value={track.pan}
onChange={onPanChange}
min={-1}
max={1}
step={0.01}
size={40}
label="PAN"
/>
</div>
{/* Vertical Volume Fader with integrated meter */}
<VerticalFader
value={track.volume}
level={track.recordEnabled || isRecording ? recordingLevel : playbackLevel}
onChange={onVolumeChange}
min={0}
max={1}
step={0.01}
showDb={true}
/>
<div className="flex-1 flex items-center justify-center min-h-0 max-h-[140px]">
<VerticalFader
value={track.volume}
level={track.recordEnabled || isRecording ? recordingLevel : playbackLevel}
onChange={onVolumeChange}
min={0}
max={1}
step={0.01}
showDb={true}
/>
</div>
</div>
)}
</div>
{/* Right: Waveform Area (Flexible Width) */}
<div
className="flex-1 relative bg-waveform-bg border-b border-border cursor-pointer"
className="flex-1 relative bg-waveform-bg border-b border-border"
onClick={onSelect}
>
{track.audioBuffer ? (
<canvas
ref={canvasRef}
className="absolute inset-0 w-full h-full cursor-crosshair"
onMouseDown={handleCanvasMouseDown}
onMouseMove={handleCanvasMouseMove}
onMouseUp={handleCanvasMouseUp}
/>
<div className="absolute inset-2 bg-card/30 border border-primary/20 rounded-sm shadow-sm overflow-hidden transition-colors hover:border-primary/40">
{/* Clip Header */}
<div className="absolute top-0 left-0 right-0 h-4 bg-gradient-to-b from-foreground/5 to-transparent pointer-events-none z-10 px-2 flex items-center">
<span className="text-[9px] text-foreground/60 font-medium truncate">
{track.name}
</span>
</div>
{/* Waveform Canvas */}
<canvas
ref={canvasRef}
className="absolute inset-0 w-full h-full cursor-crosshair"
onMouseDown={handleCanvasMouseDown}
onMouseMove={handleCanvasMouseMove}
onMouseUp={handleCanvasMouseUp}
/>
</div>
) : (
!track.collapsed && (
<>
@@ -594,7 +648,7 @@ export function Track({
{/* Bottom: Effects Section (Collapsible, Full Width) - Ableton Style */}
{!track.collapsed && (
<div className="bg-muted/80 border-b border-border shadow-inner">
<div className="bg-gradient-to-b from-muted/80 to-muted/60 border-b border-border shadow-inner">
{/* Effects Header - clickable to toggle */}
<div
className="flex items-center gap-2 px-3 py-1.5 cursor-pointer hover:bg-accent/30 transition-colors border-b border-border/30"
@@ -645,7 +699,7 @@ export function Track({
{/* Horizontal scrolling device rack - expanded state */}
{showEffects && (
<div className="h-48 overflow-x-auto custom-scrollbar bg-background/50 p-2">
<div className="h-44 overflow-x-auto custom-scrollbar bg-background/50 p-2">
<div className="flex h-full gap-2">
{track.effectChain.effects.length === 0 ? (
<div className="text-xs text-muted-foreground text-center py-8 w-full">
@@ -731,6 +785,20 @@ export function Track({
</div>
)}
{/* Track Height Resize Handle */}
{!track.collapsed && (
<div
className={cn(
'absolute bottom-0 left-0 right-0 h-1 cursor-ns-resize hover:bg-primary/50 transition-colors z-20 group',
isResizing && 'bg-primary/50'
)}
onMouseDown={handleResizeStart}
title="Drag to resize track height"
>
<div className="absolute inset-x-0 bottom-0 h-px bg-border group-hover:bg-primary" />
</div>
)}
{/* Effect Browser Dialog */}
<EffectBrowser
open={effectBrowserOpen}

View File

@@ -107,7 +107,7 @@ export function VerticalFader({
<div
ref={trackRef}
onMouseDown={handleMouseDown}
className="relative w-8 h-32 bg-muted rounded cursor-pointer select-none overflow-hidden"
className="relative w-8 flex-1 min-h-[80px] max-h-[140px] bg-muted rounded cursor-pointer select-none overflow-hidden"
>
{/* Level Meter Background (green/yellow/red gradient) */}
<div

View File

@@ -66,6 +66,7 @@ export const TRACK_COLORS: Record<TrackColor, string> = {
gray: 'rgb(156, 163, 175)',
};
export const DEFAULT_TRACK_HEIGHT = 180;
export const MIN_TRACK_HEIGHT = 60;
export const MAX_TRACK_HEIGHT = 300;
export const DEFAULT_TRACK_HEIGHT = 240; // Increased from 180 to accommodate vertical controls
export const MIN_TRACK_HEIGHT = 120; // Increased from 60 for vertical fader/knob layout
export const MAX_TRACK_HEIGHT = 400; // Increased from 300 for better waveform viewing
export const COLLAPSED_TRACK_HEIGHT = 48; // Extracted constant for collapsed state