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