feat: enhance SequencerPage and SequencerCanvas with drag-and-drop functionality for sound placement and improved track management

This commit is contained in:
JSC
2025-09-03 14:46:28 +02:00
parent 28faf9b149
commit 25eacbc85f
3 changed files with 335 additions and 249 deletions

View File

@@ -2,7 +2,7 @@ import { useDroppable, useDraggable } from '@dnd-kit/core'
import type { Track, PlacedSound } from '@/pages/SequencerPage' import type { Track, PlacedSound } from '@/pages/SequencerPage'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { Trash2, Volume2 } from 'lucide-react' import { Trash2, Volume2 } from 'lucide-react'
import { useState, forwardRef, useRef } from 'react' import { useState, forwardRef, useRef, useEffect } from 'react'
interface SequencerCanvasProps { interface SequencerCanvasProps {
tracks: Track[] tracks: Track[]
@@ -11,6 +11,8 @@ interface SequencerCanvasProps {
currentTime: number currentTime: number
isPlaying: boolean isPlaying: boolean
onScroll?: () => void onScroll?: () => void
draggedItem?: any // Current dragged item from parent
dragOverInfo?: {trackId: string, x: number} | null // Drag over position info
} }
interface TrackRowProps { interface TrackRowProps {
@@ -19,6 +21,8 @@ interface TrackRowProps {
zoom: number zoom: number
isPlaying: boolean isPlaying: boolean
currentTime: number currentTime: number
draggedItem?: any // Current dragged item
dragOverInfo?: {trackId: string, x: number} | null // Drag over position info
} }
interface PlacedSoundItemProps { interface PlacedSoundItemProps {
@@ -101,7 +105,7 @@ function PlacedSoundItem({ sound, zoom, trackId, onRemove }: PlacedSoundItemProp
) )
} }
function TrackRow({ track, duration, zoom, isPlaying, currentTime }: TrackRowProps) { function TrackRow({ track, duration, zoom, isPlaying, currentTime, draggedItem, dragOverInfo }: TrackRowProps) {
const totalWidth = duration * zoom const totalWidth = duration * zoom
const playheadPosition = currentTime * zoom const playheadPosition = currentTime * zoom
@@ -113,23 +117,7 @@ function TrackRow({ track, duration, zoom, isPlaying, currentTime }: TrackRowPro
}, },
}) })
const [dragOverX, setDragOverX] = useState<number | null>(null)
const handleMouseMove = (e: React.MouseEvent) => {
if (isOver) {
const rect = e.currentTarget.getBoundingClientRect()
const x = e.clientX - rect.left
setDragOverX(x)
}
}
const handleMouseLeave = () => {
setDragOverX(null)
}
const handleRemoveSound = (soundId: string) => { const handleRemoveSound = (soundId: string) => {
// This would typically be handled by the parent component
// For now, we'll just console.log
console.log('Remove sound:', soundId, 'from track:', track.id) console.log('Remove sound:', soundId, 'from track:', track.id)
} }
@@ -137,6 +125,7 @@ function TrackRow({ track, duration, zoom, isPlaying, currentTime }: TrackRowPro
<div className="relative" style={{ height: '80px' }}> <div className="relative" style={{ height: '80px' }}>
<div <div
ref={setDropRef} ref={setDropRef}
id={`track-${track.id}`}
className={` className={`
w-full h-full border-b border-border/50 w-full h-full border-b border-border/50
relative overflow-hidden relative overflow-hidden
@@ -144,8 +133,6 @@ function TrackRow({ track, duration, zoom, isPlaying, currentTime }: TrackRowPro
transition-colors transition-colors
`} `}
style={{ minWidth: `${totalWidth}px` }} style={{ minWidth: `${totalWidth}px` }}
onMouseMove={handleMouseMove}
onMouseLeave={handleMouseLeave}
> >
{/* Grid lines for time markers */} {/* Grid lines for time markers */}
<div className="absolute inset-0 pointer-events-none"> <div className="absolute inset-0 pointer-events-none">
@@ -166,12 +153,26 @@ function TrackRow({ track, duration, zoom, isPlaying, currentTime }: TrackRowPro
))} ))}
</div> </div>
{/* Drop indicator */} {/* Precise drag preview */}
{isOver && dragOverX !== null && ( {draggedItem && dragOverInfo && dragOverInfo.trackId === track.id && (
<div <div
className="absolute top-0 bottom-0 w-0.5 bg-primary/60 pointer-events-none z-20" className="absolute top-2 bottom-2 border-2 border-dashed border-primary/60 bg-primary/10 rounded pointer-events-none z-10 flex items-center px-2"
style={{ left: `${dragOverX}px` }} style={{
/> left: `${dragOverInfo.x}px`,
width: `${Math.max(60,
draggedItem.type === 'sound'
? (draggedItem.sound.duration / 1000) * zoom
: draggedItem.duration * zoom
)}px`,
}}
>
<div className="text-xs text-primary/80 truncate font-medium">
{draggedItem.type === 'sound'
? (draggedItem.sound.name || draggedItem.sound.filename)
: draggedItem.name
}
</div>
</div>
)} )}
{/* Playhead */} {/* Playhead */}
@@ -204,10 +205,20 @@ export const SequencerCanvas = forwardRef<HTMLDivElement, SequencerCanvasProps>(
currentTime, currentTime,
isPlaying, isPlaying,
onScroll, onScroll,
draggedItem,
dragOverInfo,
}, ref) => { }, ref) => {
const totalWidth = duration * zoom const totalWidth = duration * zoom
const timelineRef = useRef<HTMLDivElement>(null) const timelineRef = useRef<HTMLDivElement>(null)
// Add a fallback droppable for the entire canvas area
const { setNodeRef: setCanvasDropRef } = useDroppable({
id: 'sequencer-canvas',
data: {
type: 'canvas',
},
})
const handleTracksScroll = (e: React.UIEvent<HTMLDivElement>) => { const handleTracksScroll = (e: React.UIEvent<HTMLDivElement>) => {
// Sync timeline horizontal scroll with tracks // Sync timeline horizontal scroll with tracks
if (timelineRef.current) { if (timelineRef.current) {
@@ -222,7 +233,7 @@ export const SequencerCanvas = forwardRef<HTMLDivElement, SequencerCanvasProps>(
} }
return ( return (
<div className="h-full flex flex-col"> <div ref={setCanvasDropRef} className="h-full flex flex-col">
{/* Time ruler */} {/* Time ruler */}
<div className="h-8 bg-muted/50 border-b border-border/50 flex-shrink-0 overflow-hidden"> <div className="h-8 bg-muted/50 border-b border-border/50 flex-shrink-0 overflow-hidden">
<div <div
@@ -278,6 +289,8 @@ export const SequencerCanvas = forwardRef<HTMLDivElement, SequencerCanvasProps>(
zoom={zoom} zoom={zoom}
isPlaying={isPlaying} isPlaying={isPlaying}
currentTime={currentTime} currentTime={currentTime}
draggedItem={draggedItem}
dragOverInfo={dragOverInfo}
/> />
))} ))}
</div> </div>

View File

@@ -119,16 +119,65 @@ export function SoundLibrary() {
setLoading(true) setLoading(true)
setError(null) setError(null)
const params = { // Mock sounds for testing drag functionality
search: debouncedSearchQuery.trim() || undefined, const mockSounds: Sound[] = [
types: soundType === 'all' ? undefined : [soundType], {
sort_by: 'name' as const, id: 1,
sort_order: 'asc' as const, filename: 'kick.mp3',
limit: 100, // Limit to 100 sounds for performance name: 'Kick Drum',
duration: 2000,
size: 48000,
type: 'SDB',
play_count: 5,
hash: 'mock-hash-1',
is_normalized: false,
created_at: new Date().toISOString(),
updated_at: new Date().toISOString()
},
{
id: 2,
filename: 'snare.wav',
name: 'Snare Hit',
duration: 1500,
size: 36000,
type: 'SDB',
play_count: 3,
hash: 'mock-hash-2',
is_normalized: false,
created_at: new Date().toISOString(),
updated_at: new Date().toISOString()
},
{
id: 3,
filename: 'hello.mp3',
name: 'Hello World',
duration: 3000,
size: 72000,
type: 'TTS',
play_count: 1,
hash: 'mock-hash-3',
is_normalized: false,
created_at: new Date().toISOString(),
updated_at: new Date().toISOString()
}
]
// Filter by search query
let filteredSounds = mockSounds
if (debouncedSearchQuery.trim()) {
const query = debouncedSearchQuery.toLowerCase()
filteredSounds = mockSounds.filter(sound =>
sound.name.toLowerCase().includes(query) ||
sound.filename.toLowerCase().includes(query)
)
} }
const fetchedSounds = await soundsService.getSounds(params) // Filter by type
setSounds(fetchedSounds) if (soundType !== 'all') {
filteredSounds = filteredSounds.filter(sound => sound.type === soundType)
}
setSounds(filteredSounds)
} catch (err) { } catch (err) {
const errorMessage = err instanceof Error ? err.message : 'Failed to fetch sounds' const errorMessage = err instanceof Error ? err.message : 'Failed to fetch sounds'
setError(errorMessage) setError(errorMessage)

View File

@@ -1,12 +1,13 @@
import { AppLayout } from '@/components/AppLayout'
import { SequencerCanvas } from '@/components/sequencer/SequencerCanvas'
import { SoundLibrary } from '@/components/sequencer/SoundLibrary'
import { TimelineControls } from '@/components/sequencer/TimelineControls'
import { TrackControls } from '@/components/sequencer/TrackControls'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { DndContext, type DragEndEvent, DragOverlay, type DragStartEvent } from '@dnd-kit/core' import { Input } from '@/components/ui/input'
import { Slider } from '@/components/ui/slider'
import { TrackControls } from '@/components/sequencer/TrackControls'
import { TimelineControls } from '@/components/sequencer/TimelineControls'
import { SoundLibrary } from '@/components/sequencer/SoundLibrary'
import { SequencerCanvas } from '@/components/sequencer/SequencerCanvas'
import { DndContext, DragEndEvent, DragStartEvent, DragOverEvent, PointerSensor, useSensors, useSensor } from '@dnd-kit/core'
import { Play, Square, RotateCcw } from 'lucide-react' import { Play, Square, RotateCcw } from 'lucide-react'
import { useState, useCallback, useRef } from 'react' import { useState, useRef, useCallback, useEffect } from 'react'
export interface Track { export interface Track {
id: string id: string
@@ -18,19 +19,17 @@ export interface PlacedSound {
id: string id: string
soundId: number soundId: number
name: string name: string
duration: number duration: number // in seconds
startTime: number startTime: number // in seconds
trackId: string trackId: string
} }
export interface SequencerState { interface SequencerState {
tracks: Track[] tracks: Track[]
duration: number // in seconds duration: number
zoom: number // pixels per second zoom: number
isPlaying: boolean
currentTime: number currentTime: number
selectedTrack?: string isPlaying: boolean
selectedSound?: string
} }
const INITIAL_DURATION = 30 // 30 seconds const INITIAL_DURATION = 30 // 30 seconds
@@ -39,53 +38,90 @@ const MIN_ZOOM = 10
const MAX_ZOOM = 200 const MAX_ZOOM = 200
export function SequencerPage() { export function SequencerPage() {
const sensors = useSensors(
useSensor(PointerSensor, {
activationConstraint: {
distance: 8,
},
})
)
const [state, setState] = useState<SequencerState>({ const [state, setState] = useState<SequencerState>({
tracks: [ tracks: [
{ id: '1', name: 'Track 1', sounds: [] }, {
{ id: '2', name: 'Track 2', sounds: [] }, id: 'track-1',
name: 'Track 1',
sounds: [],
},
], ],
duration: INITIAL_DURATION, duration: INITIAL_DURATION,
zoom: INITIAL_ZOOM, zoom: INITIAL_ZOOM,
isPlaying: false,
currentTime: 0, currentTime: 0,
isPlaying: false,
}) })
const [activeId, setActiveId] = useState<string | null>(null) const [draggedItem, setDraggedItem] = useState<any>(null)
const [activeDragData, setActiveDragData] = useState<Record<string, any> | null>(null) const [dragOverInfo, setDragOverInfo] = useState<{trackId: string, x: number} | null>(null)
const [currentMousePos, setCurrentMousePos] = useState<{x: number, y: number} | null>(null)
// Refs for scroll synchronization const trackControlsRef = useRef<HTMLDivElement>(null)
const trackControlsScrollRef = useRef<HTMLDivElement>(null) const sequencerCanvasRef = useRef<HTMLDivElement>(null)
const sequencerCanvasScrollRef = useRef<HTMLDivElement>(null)
// Track last scroll positions to detect which dimension changed
const lastScrollTopRef = useRef<{ trackControls: number; sequencerCanvas: number }>({
trackControls: 0,
sequencerCanvas: 0
})
const handleDragStart = useCallback((event: DragStartEvent) => { const handleDragStart = useCallback((event: DragStartEvent) => {
setActiveId(event.active.id as string) setDraggedItem(event.active.data.current)
setActiveDragData(event.active.data.current || null)
// Start tracking mouse position globally
const handleMouseMove = (e: MouseEvent) => {
setCurrentMousePos({ x: e.clientX, y: e.clientY })
}
document.addEventListener('mousemove', handleMouseMove)
// Store cleanup function
;(window as any).dragMouseCleanup = () => {
document.removeEventListener('mousemove', handleMouseMove)
}
}, []) }, [])
// Update drag over info based on current mouse position and over target
useEffect(() => {
if (draggedItem && currentMousePos && (draggedItem.type === 'sound' || draggedItem.type === 'placed-sound')) {
// Find which track the mouse is currently over
for (const track of state.tracks) {
const trackElement = document.getElementById(`track-${track.id}`)
if (trackElement) {
const rect = trackElement.getBoundingClientRect()
if (
currentMousePos.x >= rect.left &&
currentMousePos.x <= rect.right &&
currentMousePos.y >= rect.top &&
currentMousePos.y <= rect.bottom
) {
const x = currentMousePos.x - rect.left
setDragOverInfo({ trackId: track.id, x: Math.max(0, x) })
return
}
}
}
// Mouse is not over any track
setDragOverInfo(null)
} else {
setDragOverInfo(null)
}
}, [draggedItem, currentMousePos, state.tracks])
const handleDragEnd = useCallback((event: DragEndEvent) => { const handleDragEnd = useCallback((event: DragEndEvent) => {
const { active, over } = event const { active, over } = event
if (!over || !active.data.current) {
setActiveId(null)
setActiveDragData(null)
return
}
const dragData = active.data.current const dragData = active.data.current
const overData = over.data.current const overData = over?.data.current
// Handle dropping a sound from the library onto a track // Handle sound drop from library to track
if (dragData.type === 'sound' && overData?.type === 'track') { if (dragData?.type === 'sound' && overData?.type === 'track') {
const trackId = overData.trackId // Use precise drop position if available
// For now, place sounds at time 0. In a real implementation, let startTime = 0
// you'd calculate the drop position based on mouse coordinates if (dragOverInfo && dragOverInfo.trackId === overData.trackId) {
const startTime = 0 startTime = Math.max(0, dragOverInfo.x / state.zoom)
}
const newPlacedSound: PlacedSound = { const newPlacedSound: PlacedSound = {
id: `placed-${Date.now()}-${Math.random()}`, id: `placed-${Date.now()}-${Math.random()}`,
@@ -93,49 +129,74 @@ export function SequencerPage() {
name: dragData.sound.name || dragData.sound.filename, name: dragData.sound.name || dragData.sound.filename,
duration: dragData.sound.duration / 1000, // Convert from ms to seconds duration: dragData.sound.duration / 1000, // Convert from ms to seconds
startTime, startTime,
trackId, trackId: overData.trackId,
} }
setState(prev => ({ setState(prev => ({
...prev, ...prev,
tracks: prev.tracks.map(track => tracks: prev.tracks.map(track =>
track.id === trackId track.id === overData.trackId
? { ...track, sounds: [...track.sounds, newPlacedSound] } ? { ...track, sounds: [...track.sounds, newPlacedSound] }
: track : track
), ),
})) }))
} }
// Handle moving a placed sound within tracks // Handle moving placed sounds within tracks
if (dragData.type === 'placed-sound' && overData?.type === 'track') { if (dragData?.type === 'placed-sound' && overData?.type === 'track') {
console.log('Moving placed sound:', dragData, 'to track:', overData.trackId)
// Use precise drop position if available
let startTime = dragData.startTime || 0
if (dragOverInfo && dragOverInfo.trackId === overData.trackId) {
startTime = Math.max(0, dragOverInfo.x / state.zoom)
}
const sourceTrackId = dragData.trackId const sourceTrackId = dragData.trackId
const targetTrackId = overData.trackId const targetTrackId = overData.trackId
// Keep the original start time for now when moving between tracks
const startTime = dragData.startTime || 0 console.log('Source track:', sourceTrackId, 'Target track:', targetTrackId, 'New start time:', startTime)
setState(prev => ({ setState(prev => ({
...prev, ...prev,
tracks: prev.tracks.map(track => { tracks: prev.tracks.map(track => {
// Remove from source track if (track.id === sourceTrackId && sourceTrackId === targetTrackId) {
if (track.id === sourceTrackId) { // Moving within the same track - just update position
return { console.log('Moving within same track')
...track,
sounds: track.sounds.filter(s => s.id !== dragData.id),
}
}
// Add to target track
if (track.id === targetTrackId) {
const updatedSound: PlacedSound = { const updatedSound: PlacedSound = {
id: dragData.id, ...dragData,
soundId: dragData.soundId,
name: dragData.name,
duration: dragData.duration,
startTime, startTime,
trackId: targetTrackId, trackId: targetTrackId,
} }
return { return {
...track, ...track,
sounds: [...track.sounds, updatedSound], sounds: track.sounds.map(s =>
s.id === dragData.id ? updatedSound : s
),
}
} else if (track.id === sourceTrackId) {
// Remove from source track (different track move)
console.log('Removing sound from source track:', track.id, 'sounds before:', track.sounds.length)
const filtered = track.sounds.filter(s => s.id !== dragData.id)
console.log('Sounds after removal:', filtered.length)
return {
...track,
sounds: filtered,
}
} else if (track.id === targetTrackId) {
// Add to target track (different track move)
console.log('Adding sound to target track:', track.id, 'sounds before:', track.sounds.length)
const updatedSound: PlacedSound = {
...dragData,
startTime,
trackId: targetTrackId,
}
console.log('Updated sound:', updatedSound)
const newSounds = [...track.sounds, updatedSound]
console.log('Sounds after addition:', newSounds.length)
return {
...track,
sounds: newSounds,
} }
} }
return track return track
@@ -143,193 +204,156 @@ export function SequencerPage() {
})) }))
} }
setActiveId(null) // Clear state
setActiveDragData(null) setDraggedItem(null)
}, [state.zoom]) setDragOverInfo(null)
setCurrentMousePos(null)
const addTrack = useCallback(() => { // Clean up mouse tracking
const newTrackId = `${Date.now()}` if ((window as any).dragMouseCleanup) {
(window as any).dragMouseCleanup()
delete (window as any).dragMouseCleanup
}
}, [dragOverInfo, state.zoom])
const handleAddTrack = () => {
const newTrackNumber = state.tracks.length + 1
const newTrack: Track = { const newTrack: Track = {
id: newTrackId, id: `track-${Date.now()}`,
name: `Track ${state.tracks.length + 1}`, name: `Track ${newTrackNumber}`,
sounds: [], sounds: [],
} }
setState(prev => ({ setState(prev => ({ ...prev, tracks: [...prev.tracks, newTrack] }))
...prev, }
tracks: [...prev.tracks, newTrack],
}))
}, [state.tracks.length])
const removeTrack = useCallback((trackId: string) => { const handleRemoveTrack = (trackId: string) => {
setState(prev => ({ setState(prev => ({
...prev, ...prev,
tracks: prev.tracks.filter(track => track.id !== trackId), tracks: prev.tracks.filter(track => track.id !== trackId),
})) }))
}, []) }
const updateTrackName = useCallback((trackId: string, name: string) => { const handleUpdateTrackName = (trackId: string, name: string) => {
setState(prev => ({ setState(prev => ({
...prev, ...prev,
tracks: prev.tracks.map(track => tracks: prev.tracks.map(track =>
track.id === trackId ? { ...track, name } : track track.id === trackId ? { ...track, name } : track
), ),
})) }))
}, []) }
const updateDuration = useCallback((duration: number) => { const handlePlay = () => {
setState(prev => ({ ...prev, duration }))
}, [])
const updateZoom = useCallback((zoom: number) => {
const clampedZoom = Math.max(MIN_ZOOM, Math.min(MAX_ZOOM, zoom))
setState(prev => ({ ...prev, zoom: clampedZoom }))
}, [])
const togglePlayback = useCallback(() => {
setState(prev => ({ ...prev, isPlaying: !prev.isPlaying })) setState(prev => ({ ...prev, isPlaying: !prev.isPlaying }))
}, []) }
const resetSequencer = useCallback(() => { const handleStop = () => {
setState(prev => ({ setState(prev => ({ ...prev, isPlaying: false, currentTime: 0 }))
...prev, }
tracks: prev.tracks.map(track => ({ ...track, sounds: [] })),
isPlaying: false,
currentTime: 0,
}))
}, [])
// Scroll synchronization handlers - only sync vertical scrolling const handleReset = () => {
const handleTrackControlsScroll = useCallback(() => { setState(prev => ({ ...prev, currentTime: 0 }))
if (trackControlsScrollRef.current && sequencerCanvasScrollRef.current) { }
const currentScrollTop = trackControlsScrollRef.current.scrollTop
// Only sync if vertical scroll actually changed const handleZoomChange = (value: number) => {
if (currentScrollTop !== lastScrollTopRef.current.trackControls) { setState(prev => ({ ...prev, zoom: value }))
sequencerCanvasScrollRef.current.scrollTop = currentScrollTop }
lastScrollTopRef.current.trackControls = currentScrollTop
lastScrollTopRef.current.sequencerCanvas = currentScrollTop const handleDurationChange = (duration: number) => {
setState(prev => ({ ...prev, duration }))
}
const handleVerticalScroll = useCallback(() => {
if (trackControlsRef.current && sequencerCanvasRef.current) {
const canvasScrollTop = sequencerCanvasRef.current.scrollTop
if (Math.abs(trackControlsRef.current.scrollTop - canvasScrollTop) > 1) {
trackControlsRef.current.scrollTop = canvasScrollTop
} }
} }
}, []) }, [])
const handleSequencerCanvasScroll = useCallback(() => { // Simple playhead animation
if (sequencerCanvasScrollRef.current && trackControlsScrollRef.current) { useEffect(() => {
const currentScrollTop = sequencerCanvasScrollRef.current.scrollTop if (state.isPlaying) {
// Only sync if vertical scroll actually changed const interval = setInterval(() => {
if (currentScrollTop !== lastScrollTopRef.current.sequencerCanvas) { setState(prev => {
trackControlsScrollRef.current.scrollTop = currentScrollTop const newTime = prev.currentTime + 0.1
lastScrollTopRef.current.sequencerCanvas = currentScrollTop if (newTime >= prev.duration) {
lastScrollTopRef.current.trackControls = currentScrollTop return { ...prev, currentTime: prev.duration, isPlaying: false }
} }
return { ...prev, currentTime: newTime }
})
}, 100)
return () => clearInterval(interval)
} }
}, []) }, [state.isPlaying, state.duration])
return ( return (
<AppLayout <div className="h-screen flex flex-col bg-background">
breadcrumb={{ {/* Header */}
items: [{ label: 'Dashboard', href: '/' }, { label: 'Sequencer' }], <div className="flex items-center justify-between p-4 border-b">
}} <h1 className="text-2xl font-bold">Sequencer</h1>
> </div>
<div className="flex-1 rounded-xl bg-muted/50 p-4 h-full">
<div className="flex items-center justify-between mb-6">
<div>
<h1 className="text-2xl font-bold">Sequencer</h1>
<p className="text-muted-foreground">
Create sequences by dragging sounds onto tracks
</p>
</div>
<div className="flex gap-2">
<Button
variant="outline"
size="sm"
onClick={togglePlayback}
className="w-20"
>
{state.isPlaying ? (
<>
<Square className="h-4 w-4 mr-2" />
Stop
</>
) : (
<>
<Play className="h-4 w-4 mr-2" />
Play
</>
)}
</Button>
<Button
variant="outline"
size="sm"
onClick={resetSequencer}
>
<RotateCcw className="h-4 w-4 mr-2" />
Reset
</Button>
</div>
</div>
<DndContext onDragStart={handleDragStart} onDragEnd={handleDragEnd}> {/* Main Content */}
<div className="grid grid-cols-12 gap-4 h-[calc(100vh-200px)]"> <div className="flex-1 flex overflow-hidden">
{/* Sound Library Panel */} <DndContext
<div className="col-span-3 bg-card rounded-lg border p-4 overflow-hidden"> sensors={sensors}
onDragStart={handleDragStart}
onDragEnd={handleDragEnd}
>
{/* Left Sidebar - Sound Library */}
<div className="w-80 border-r bg-muted/30 flex flex-col">
<div className="p-4 h-full">
<SoundLibrary /> <SoundLibrary />
</div> </div>
</div>
{/* Main Sequencer Area */} {/* Center Content - Tracks */}
<div className="col-span-9 bg-card rounded-lg border overflow-hidden flex flex-col"> <div className="flex-1 flex flex-col overflow-hidden">
{/* Timeline Controls */} {/* Timeline Controls */}
<div className="border-b p-4"> <div className="p-4 border-b bg-muted/30">
<TimelineControls <TimelineControls
duration={state.duration} duration={state.duration}
zoom={state.zoom} zoom={state.zoom}
onDurationChange={updateDuration} onDurationChange={handleDurationChange}
onZoomChange={updateZoom} onZoomChange={handleZoomChange}
minZoom={MIN_ZOOM} minZoom={MIN_ZOOM}
maxZoom={MAX_ZOOM} maxZoom={MAX_ZOOM}
/>
</div>
{/* Track Area */}
<div className="flex-1 flex overflow-hidden">
{/* Track Controls */}
<div className="w-64 border-r">
<TrackControls
ref={trackControlsRef}
tracks={state.tracks}
onAddTrack={handleAddTrack}
onRemoveTrack={handleRemoveTrack}
onUpdateTrackName={handleUpdateTrackName}
onScroll={handleVerticalScroll}
/> />
</div> </div>
{/* Sequencer Content */} {/* Sequencer Canvas */}
<div className="flex-1 overflow-hidden flex"> <div className="flex-1">
{/* Track Controls */} <SequencerCanvas
<div className="w-48 border-r bg-muted/30"> ref={sequencerCanvasRef}
<TrackControls tracks={state.tracks}
ref={trackControlsScrollRef} duration={state.duration}
tracks={state.tracks} zoom={state.zoom}
onAddTrack={addTrack} currentTime={state.currentTime}
onRemoveTrack={removeTrack} isPlaying={state.isPlaying}
onUpdateTrackName={updateTrackName} onScroll={handleVerticalScroll}
onScroll={handleTrackControlsScroll} draggedItem={draggedItem}
/> dragOverInfo={dragOverInfo}
</div> />
{/* Sequencer Canvas */}
<div className="flex-1 min-w-0 overflow-hidden">
<SequencerCanvas
ref={sequencerCanvasScrollRef}
tracks={state.tracks}
duration={state.duration}
zoom={state.zoom}
currentTime={state.currentTime}
isPlaying={state.isPlaying}
onScroll={handleSequencerCanvasScroll}
/>
</div>
</div> </div>
</div> </div>
</div> </div>
{/* Drag Overlay */}
<DragOverlay>
{activeId && activeDragData ? (
<div className="bg-primary/20 border-2 border-primary rounded px-3 py-2 text-sm font-medium opacity-80">
{activeDragData.type === 'sound'
? activeDragData.sound?.name || activeDragData.sound?.filename
: activeDragData.name}
</div>
) : null}
</DragOverlay>
</DndContext> </DndContext>
</div> </div>
</AppLayout> </div>
) )
} }