feat: enhance mobile responsiveness with collapsible controls and automation/effects bars

Added comprehensive mobile support for Phase 15 (Polish & Optimization):

**Mobile Layout Enhancements:**
- Track controls now collapsible on mobile with two states:
  - Collapsed: minimal controls with expand chevron, R/M/S buttons, horizontal level meter
  - Expanded: full height fader, pan control, all buttons
- Track collapse buttons added to mobile view (left chevron for track collapse, right chevron for control collapse)
- Master controls collapse button hidden on desktop (lg:hidden)
- Automation and effects bars now available on mobile layout
- Both bars collapsible with eye/eye-off icons, horizontally scrollable when zoomed
- Mobile vertical stacking: controls → waveform → automation → effects per track

**Bug Fixes:**
- Fixed track controls and waveform container height matching on desktop
- Fixed Modal component prop: isOpen → open in all dialog components
- Fixed TypeScript null check for audioBuffer.duration
- Fixed keyboard shortcut category: 'help' → 'view'

**Technical Improvements:**
- Consistent height calculation using trackHeight variable
- Proper responsive breakpoints with Tailwind (sm:640px, lg:1024px)
- Progressive disclosure pattern for mobile controls

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-11-19 20:50:44 +01:00
parent e09bc1449c
commit 908e6caaf8
13 changed files with 2136 additions and 126 deletions

View File

@@ -4,16 +4,9 @@ import * as React from 'react';
import { Music, Plus, Upload, Trash2, Settings, Download, FolderOpen } from 'lucide-react';
import { PlaybackControls } from './PlaybackControls';
import { MasterControls } from '@/components/controls/MasterControls';
import { FrequencyAnalyzer } from '@/components/analysis/FrequencyAnalyzer';
import { Spectrogram } from '@/components/analysis/Spectrogram';
import { PhaseCorrelationMeter } from '@/components/analysis/PhaseCorrelationMeter';
import { LUFSMeter } from '@/components/analysis/LUFSMeter';
import { AudioStatistics } from '@/components/analysis/AudioStatistics';
import { ThemeToggle } from '@/components/layout/ThemeToggle';
import { CommandPalette } from '@/components/ui/CommandPalette';
import { GlobalSettingsDialog } from '@/components/settings/GlobalSettingsDialog';
import { ExportDialog, type ExportSettings } from '@/components/dialogs/ExportDialog';
import { ProjectsDialog } from '@/components/dialogs/ProjectsDialog';
import { BrowserCompatDialog } from '@/components/dialogs/BrowserCompatDialog';
import { Button } from '@/components/ui/Button';
import type { CommandAction } from '@/components/ui/CommandPalette';
import { useMultiTrack } from '@/lib/hooks/useMultiTrack';
@@ -21,7 +14,21 @@ import { useMultiTrackPlayer } from '@/lib/hooks/useMultiTrackPlayer';
import { useEffectChain } from '@/lib/hooks/useEffectChain';
import { useToast } from '@/components/ui/Toast';
import { TrackList } from '@/components/tracks/TrackList';
import { ImportTrackDialog } from '@/components/tracks/ImportTrackDialog';
import type { ExportSettings } from '@/components/dialogs/ExportDialog';
// Lazy load dialogs for better performance
const GlobalSettingsDialog = React.lazy(() => import('@/components/settings/GlobalSettingsDialog').then(m => ({ default: m.GlobalSettingsDialog })));
const ExportDialog = React.lazy(() => import('@/components/dialogs/ExportDialog').then(m => ({ default: m.ExportDialog })));
const ProjectsDialog = React.lazy(() => import('@/components/dialogs/ProjectsDialog').then(m => ({ default: m.ProjectsDialog })));
const ImportTrackDialog = React.lazy(() => import('@/components/tracks/ImportTrackDialog').then(m => ({ default: m.ImportTrackDialog })));
const KeyboardShortcutsDialog = React.lazy(() => import('@/components/dialogs/KeyboardShortcutsDialog').then(m => ({ default: m.KeyboardShortcutsDialog })));
// Lazy load analysis components (shown conditionally based on analyzerView)
const FrequencyAnalyzer = React.lazy(() => import('@/components/analysis/FrequencyAnalyzer').then(m => ({ default: m.FrequencyAnalyzer })));
const Spectrogram = React.lazy(() => import('@/components/analysis/Spectrogram').then(m => ({ default: m.Spectrogram })));
const PhaseCorrelationMeter = React.lazy(() => import('@/components/analysis/PhaseCorrelationMeter').then(m => ({ default: m.PhaseCorrelationMeter })));
const LUFSMeter = React.lazy(() => import('@/components/analysis/LUFSMeter').then(m => ({ default: m.LUFSMeter })));
const AudioStatistics = React.lazy(() => import('@/components/analysis/AudioStatistics').then(m => ({ default: m.AudioStatistics })));
import { formatDuration } from '@/lib/audio/decoder';
import { useHistory } from '@/lib/hooks/useHistory';
import { useRecording } from '@/lib/hooks/useRecording';
@@ -48,6 +55,7 @@ import {
type ProjectMetadata,
} from '@/lib/storage/projects';
import { getAudioContext } from '@/lib/audio/context';
import { checkBrowserCompatibility } from '@/lib/utils/browser-compat';
export function AudioEditor() {
// Settings hook
@@ -66,6 +74,7 @@ export function AudioEditor() {
const [masterVolume, setMasterVolume] = React.useState(0.8);
const [masterPan, setMasterPan] = React.useState(0);
const [isMasterMuted, setIsMasterMuted] = React.useState(false);
const [masterControlsCollapsed, setMasterControlsCollapsed] = React.useState(false);
const [clipboard, setClipboard] = React.useState<AudioBuffer | null>(null);
const [recordingTrackId, setRecordingTrackId] = React.useState<string | null>(null);
const [punchInEnabled, setPunchInEnabled] = React.useState(false);
@@ -87,6 +96,12 @@ export function AudioEditor() {
const [projects, setProjects] = React.useState<ProjectMetadata[]>([]);
const [currentProjectId, setCurrentProjectId] = React.useState<string | null>(null);
const [currentProjectName, setCurrentProjectName] = React.useState('Untitled Project');
const [browserCompatDialogOpen, setBrowserCompatDialogOpen] = React.useState(false);
const [shortcutsDialogOpen, setShortcutsDialogOpen] = React.useState(false);
const [browserCompatInfo, setBrowserCompatInfo] = React.useState<{
missingFeatures: string[];
warnings: string[];
}>({ missingFeatures: [], warnings: [] });
const { addToast } = useToast();
@@ -633,6 +648,9 @@ export function AudioEditor() {
);
setClipboard(extracted);
// Ensure the track is selected so paste works
setSelectedTrackId(track.id);
// Execute cut command
const command = createMultiTrackCutCommand(
track.id,
@@ -664,6 +682,9 @@ export function AudioEditor() {
);
setClipboard(extracted);
// Ensure the track is selected so paste works
setSelectedTrackId(track.id);
// Execute copy command (doesn't modify buffer, just for undo history)
const command = createMultiTrackCopyCommand(
track.id,
@@ -684,7 +705,25 @@ export function AudioEditor() {
}, [tracks, executeCommand, updateTrack, addToast]);
const handlePaste = React.useCallback(() => {
if (!clipboard || !selectedTrackId) return;
if (!clipboard) {
addToast({
title: 'Nothing to Paste',
description: 'Clipboard is empty. Copy or cut a selection first.',
variant: 'error',
duration: 2000,
});
return;
}
if (!selectedTrackId) {
addToast({
title: 'No Track Selected',
description: 'Select a track to paste into.',
variant: 'error',
duration: 2000,
});
return;
}
const track = tracks.find((t) => t.id === selectedTrackId);
if (!track) return;
@@ -1056,6 +1095,16 @@ export function AudioEditor() {
}
}, [loadTracks, addToast]);
// Check browser compatibility on mount
React.useEffect(() => {
const { isSupported, missingFeatures, warnings } = checkBrowserCompatibility();
if (!isSupported || warnings.length > 0) {
setBrowserCompatInfo({ missingFeatures, warnings });
setBrowserCompatDialogOpen(true);
}
}, []);
// Auto-load last project on mount
const [hasAutoLoaded, setHasAutoLoaded] = React.useState(false);
React.useEffect(() => {
@@ -1266,6 +1315,80 @@ export function AudioEditor() {
category: 'playback',
action: stop,
},
// Edit
{
id: 'undo',
label: 'Undo',
description: 'Undo last action',
shortcut: 'Ctrl+Z',
category: 'edit',
action: undo,
},
{
id: 'redo',
label: 'Redo',
description: 'Redo last undone action',
shortcut: 'Ctrl+Shift+Z',
category: 'edit',
action: redo,
},
{
id: 'cut',
label: 'Cut',
description: 'Cut selection to clipboard',
shortcut: 'Ctrl+X',
category: 'edit',
action: handleCut,
},
{
id: 'copy',
label: 'Copy',
description: 'Copy selection to clipboard',
shortcut: 'Ctrl+C',
category: 'edit',
action: handleCopy,
},
{
id: 'paste',
label: 'Paste',
description: 'Paste clipboard content',
shortcut: 'Ctrl+V',
category: 'edit',
action: handlePaste,
},
{
id: 'delete',
label: 'Delete',
description: 'Delete selection',
shortcut: 'Delete',
category: 'edit',
action: handleDelete,
},
{
id: 'duplicate',
label: 'Duplicate',
description: 'Duplicate selection',
shortcut: 'Ctrl+D',
category: 'edit',
action: handleDuplicate,
},
{
id: 'select-all',
label: 'Select All',
description: 'Select all content on current track',
shortcut: 'Ctrl+A',
category: 'edit',
action: () => {
if (selectedTrackId) {
const track = tracks.find(t => t.id === selectedTrackId);
if (track?.audioBuffer) {
updateTrack(selectedTrackId, {
selection: { start: 0, end: track.audioBuffer.duration }
});
}
}
},
},
// Project
{
id: 'save-project',
@@ -1287,6 +1410,7 @@ export function AudioEditor() {
id: 'zoom-in',
label: 'Zoom In',
description: 'Zoom in on waveforms',
shortcut: 'Ctrl++',
category: 'view',
action: handleZoomIn,
},
@@ -1294,6 +1418,7 @@ export function AudioEditor() {
id: 'zoom-out',
label: 'Zoom Out',
description: 'Zoom out on waveforms',
shortcut: 'Ctrl+-',
category: 'view',
action: handleZoomOut,
},
@@ -1301,9 +1426,59 @@ export function AudioEditor() {
id: 'fit-to-view',
label: 'Fit to View',
description: 'Reset zoom to fit all tracks',
shortcut: 'Ctrl+0',
category: 'view',
action: handleFitToView,
},
// Navigation
{
id: 'seek-start',
label: 'Go to Start',
description: 'Seek to beginning',
shortcut: 'Home',
category: 'playback',
action: () => seek(0),
},
{
id: 'seek-end',
label: 'Go to End',
description: 'Seek to end',
shortcut: 'End',
category: 'playback',
action: () => seek(duration),
},
{
id: 'seek-backward',
label: 'Seek Backward',
description: 'Seek backward 1 second',
shortcut: '←',
category: 'playback',
action: () => seek(Math.max(0, currentTime - 1)),
},
{
id: 'seek-forward',
label: 'Seek Forward',
description: 'Seek forward 1 second',
shortcut: '→',
category: 'playback',
action: () => seek(Math.min(duration, currentTime + 1)),
},
{
id: 'seek-backward-5s',
label: 'Seek Backward 5s',
description: 'Seek backward 5 seconds',
shortcut: 'Ctrl+←',
category: 'playback',
action: () => seek(Math.max(0, currentTime - 5)),
},
{
id: 'seek-forward-5s',
label: 'Seek Forward 5s',
description: 'Seek forward 5 seconds',
shortcut: 'Ctrl+→',
category: 'playback',
action: () => seek(Math.min(duration, currentTime + 5)),
},
// Tracks
{
id: 'add-track',
@@ -1326,9 +1501,18 @@ export function AudioEditor() {
category: 'tracks',
action: handleClearTracks,
},
// Help
{
id: 'keyboard-shortcuts',
label: 'Keyboard Shortcuts',
description: 'View all keyboard shortcuts',
shortcut: '?',
category: 'view',
action: () => setShortcutsDialogOpen(true),
},
];
return actions;
}, [play, pause, stop, handleSaveProject, handleOpenProjectsDialog, handleZoomIn, handleZoomOut, handleFitToView, handleImportTracks, handleClearTracks, addTrack]);
}, [play, pause, stop, undo, redo, handleCut, handleCopy, handlePaste, handleDelete, handleDuplicate, handleSaveProject, handleOpenProjectsDialog, handleZoomIn, handleZoomOut, handleFitToView, handleImportTracks, handleClearTracks, addTrack, seek, duration, currentTime, selectedTrackId, tracks, updateTrack, setShortcutsDialogOpen]);
// Keyboard shortcuts
React.useEffect(() => {
@@ -1398,6 +1582,27 @@ export function AudioEditor() {
return;
}
// Ctrl/Cmd++: Zoom In (also accepts Ctrl+=)
if ((e.ctrlKey || e.metaKey) && (e.key === '=' || e.key === '+')) {
e.preventDefault();
handleZoomIn();
return;
}
// Ctrl/Cmd+-: Zoom Out
if ((e.ctrlKey || e.metaKey) && e.key === '-') {
e.preventDefault();
handleZoomOut();
return;
}
// Ctrl/Cmd+0: Fit to View
if ((e.ctrlKey || e.metaKey) && e.key === '0') {
e.preventDefault();
handleFitToView();
return;
}
// Delete or Backspace: Delete selection
if (e.key === 'Delete' || e.key === 'Backspace') {
e.preventDefault();
@@ -1457,7 +1662,16 @@ export function AudioEditor() {
// Ctrl+A: Select All (select all content on current track)
if ((e.ctrlKey || e.metaKey) && e.key === 'a') {
e.preventDefault();
if (selectedTrackId) {
if (!selectedTrackId) {
// If no track selected, try to select the first track with audio
const trackWithAudio = tracks.find(t => t.audioBuffer);
if (trackWithAudio && trackWithAudio.audioBuffer) {
setSelectedTrackId(trackWithAudio.id);
updateTrack(trackWithAudio.id, {
selection: { start: 0, end: trackWithAudio.audioBuffer.duration }
});
}
} else {
const track = tracks.find(t => t.id === selectedTrackId);
if (track?.audioBuffer) {
updateTrack(selectedTrackId, {
@@ -1468,45 +1682,31 @@ export function AudioEditor() {
return;
}
// Ctrl+Plus/Equals: Zoom in
if ((e.ctrlKey || e.metaKey) && (e.key === '+' || e.key === '=')) {
// ?: Open keyboard shortcuts help
if (e.key === '?' && !e.ctrlKey && !e.metaKey) {
e.preventDefault();
handleZoomIn();
return;
}
// Ctrl+Minus: Zoom out
if ((e.ctrlKey || e.metaKey) && e.key === '-') {
e.preventDefault();
handleZoomOut();
return;
}
// Ctrl+0: Fit to view
if ((e.ctrlKey || e.metaKey) && e.key === '0') {
e.preventDefault();
handleFitToView();
setShortcutsDialogOpen(true);
return;
}
};
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, [togglePlayPause, canUndo, canRedo, undo, redo, handleCut, handleCopy, handlePaste, handleDelete, handleDuplicate, handleSaveProject, seek, duration, currentTime, selectedTrackId, tracks, updateTrack, handleZoomIn, handleZoomOut, handleFitToView]);
}, [togglePlayPause, canUndo, canRedo, undo, redo, handleCut, handleCopy, handlePaste, handleDelete, handleDuplicate, handleSaveProject, seek, duration, currentTime, selectedTrackId, tracks, updateTrack, handleZoomIn, handleZoomOut, handleFitToView, setSelectedTrackId, setShortcutsDialogOpen]);
return (
<>
{/* Compact Header */}
<header className="flex items-center justify-between px-4 py-2 border-b border-border bg-card flex-shrink-0 gap-4">
<header className="flex items-center justify-between px-2 sm:px-4 py-2 border-b border-border bg-card flex-shrink-0 gap-2 sm:gap-4">
{/* Left: Logo */}
<div className="flex items-center gap-4 flex-shrink-0">
<div className="flex items-center gap-2 sm:gap-4 flex-shrink-0">
<div className="flex items-center gap-2">
<Music className="h-5 w-5 text-primary" />
<h1 className="text-lg font-bold text-foreground">Audio UI</h1>
<h1 className="text-base sm:text-lg font-bold text-foreground hidden sm:block">Audio UI</h1>
</div>
{/* Project Name */}
<div className="flex items-center gap-2 border-l border-border pl-4">
<div className="hidden md:flex items-center gap-2 border-l border-border pl-4">
<input
type="text"
value={currentProjectName}
@@ -1518,27 +1718,31 @@ export function AudioEditor() {
/>
</div>
{/* Track Actions */}
<div className="flex items-center gap-2 border-l border-border pl-4">
<Button variant="outline" size="sm" onClick={handleOpenProjectsDialog}>
{/* Track Actions - Compact on mobile */}
<div className="flex items-center gap-1 sm:gap-2 border-l border-border pl-2 sm:pl-4">
<Button variant="outline" size="sm" onClick={handleOpenProjectsDialog} className="hidden sm:flex">
<FolderOpen className="h-4 w-4 mr-1.5" />
Projects
</Button>
<Button variant="outline" size="sm" onClick={handleOpenProjectsDialog} className="sm:hidden px-2">
<FolderOpen className="h-4 w-4" />
</Button>
<Button variant="outline" size="sm" onClick={() => addTrack()}>
<Plus className="h-4 w-4 mr-1.5" />
Add Track
<Plus className="h-4 w-4 sm:mr-1.5" />
<span className="hidden sm:inline">Add Track</span>
</Button>
<Button variant="outline" size="sm" onClick={handleImportTracks}>
<Upload className="h-4 w-4 mr-1.5" />
Import
<Upload className="h-4 w-4 sm:mr-1.5" />
<span className="hidden sm:inline">Import</span>
</Button>
{tracks.length > 0 && (
<>
<Button variant="outline" size="sm" onClick={() => setExportDialogOpen(true)}>
<Button variant="outline" size="sm" onClick={() => setExportDialogOpen(true)} className="hidden md:flex">
<Download className="h-4 w-4 mr-1.5" />
Export
</Button>
<Button variant="outline" size="sm" onClick={handleClearTracks}>
<Button variant="outline" size="sm" onClick={handleClearTracks} className="hidden lg:flex">
<Trash2 className="h-4 w-4 mr-1.5 text-destructive" />
Clear All
</Button>
@@ -1591,8 +1795,8 @@ export function AudioEditor() {
</div>
</main>
{/* Right Sidebar - Master Controls & Analyzers */}
<aside className="flex-shrink-0 border-l border-border bg-card flex flex-col pt-5 px-4 pb-4 gap-4 w-60">
{/* Right Sidebar - Master Controls & Analyzers - Hidden on mobile */}
<aside className="hidden lg:flex flex-shrink-0 border-l border-border bg-card flex-col pt-5 px-4 pb-4 gap-4 w-60">
{/* Master Controls */}
<div className="flex items-center justify-center">
<MasterControls
@@ -1602,6 +1806,7 @@ export function AudioEditor() {
rmsLevel={masterRmsLevel}
isClipping={masterIsClipping}
isMuted={isMasterMuted}
collapsed={masterControlsCollapsed}
onVolumeChange={setMasterVolume}
onPanChange={setMasterPan}
onMuteToggle={() => {
@@ -1614,6 +1819,7 @@ export function AudioEditor() {
}
}}
onResetClip={resetClipIndicator}
onToggleCollapse={() => setMasterControlsCollapsed(!masterControlsCollapsed)}
/>
</div>
@@ -1681,89 +1887,162 @@ export function AudioEditor() {
{/* Analyzer Display */}
<div className="flex-1 min-h-[360px] flex items-start justify-center">
<div className="w-[178px]">
{analyzerView === 'frequency' && <FrequencyAnalyzer analyserNode={masterAnalyser} />}
{analyzerView === 'spectrogram' && settings.performance.enableSpectrogram && <Spectrogram analyserNode={masterAnalyser} />}
{analyzerView === 'phase' && <PhaseCorrelationMeter analyserNode={masterAnalyser} />}
{analyzerView === 'lufs' && <LUFSMeter analyserNode={masterAnalyser} />}
{analyzerView === 'stats' && <AudioStatistics tracks={tracks} />}
{analyzerView === 'frequency' && (
<React.Suspense fallback={<div className="h-[300px] flex items-center justify-center text-muted-foreground">Loading...</div>}>
<FrequencyAnalyzer analyserNode={masterAnalyser} />
</React.Suspense>
)}
{analyzerView === 'spectrogram' && settings.performance.enableSpectrogram && (
<React.Suspense fallback={<div className="h-[300px] flex items-center justify-center text-muted-foreground">Loading...</div>}>
<Spectrogram analyserNode={masterAnalyser} />
</React.Suspense>
)}
{analyzerView === 'phase' && (
<React.Suspense fallback={<div className="h-[300px] flex items-center justify-center text-muted-foreground">Loading...</div>}>
<PhaseCorrelationMeter analyserNode={masterAnalyser} />
</React.Suspense>
)}
{analyzerView === 'lufs' && (
<React.Suspense fallback={<div className="h-[300px] flex items-center justify-center text-muted-foreground">Loading...</div>}>
<LUFSMeter analyserNode={masterAnalyser} />
</React.Suspense>
)}
{analyzerView === 'stats' && (
<React.Suspense fallback={<div className="h-[300px] flex items-center justify-center text-muted-foreground">Loading...</div>}>
<AudioStatistics tracks={tracks} />
</React.Suspense>
)}
</div>
</div>
</aside>
</div>
{/* Transport Controls */}
<div className="border-t border-border bg-card p-3 flex justify-center">
<PlaybackControls
isPlaying={isPlaying}
isPaused={!isPlaying}
currentTime={currentTime}
duration={duration}
volume={masterVolume}
onPlay={play}
onPause={pause}
onStop={stop}
onSeek={seek}
onVolumeChange={setMasterVolume}
currentTimeFormatted={formatDuration(currentTime)}
durationFormatted={formatDuration(duration)}
isRecording={recordingState.isRecording}
onStartRecording={handleStartRecording}
onStopRecording={handleStopRecording}
punchInEnabled={punchInEnabled}
punchInTime={punchInTime}
punchOutTime={punchOutTime}
onPunchInEnabledChange={setPunchInEnabled}
onPunchInTimeChange={setPunchInTime}
onPunchOutTimeChange={setPunchOutTime}
overdubEnabled={overdubEnabled}
onOverdubEnabledChange={setOverdubEnabled}
/>
{/* Bottom Bar - Stacked on mobile (Master then Transport), Side-by-side on desktop */}
<div className="border-t border-border bg-card p-3 flex flex-col lg:flex-row gap-3">
{/* Master Controls - Mobile only (hidden on desktop where sidebar shows master) */}
<div className="lg:hidden flex justify-center border-b border-border pb-3 lg:border-b-0 lg:pb-0">
<MasterControls
volume={masterVolume}
pan={masterPan}
peakLevel={masterPeakLevel}
rmsLevel={masterRmsLevel}
isClipping={masterIsClipping}
isMuted={isMasterMuted}
collapsed={masterControlsCollapsed}
onVolumeChange={setMasterVolume}
onPanChange={setMasterPan}
onMuteToggle={() => {
if (isMasterMuted) {
setMasterVolume(0.8);
setIsMasterMuted(false);
} else {
setMasterVolume(0);
setIsMasterMuted(true);
}
}}
onResetClip={resetClipIndicator}
onToggleCollapse={() => setMasterControlsCollapsed(!masterControlsCollapsed)}
/>
</div>
{/* Playback Controls - Bottom on mobile, centered on desktop */}
<div className="flex-1 flex justify-center">
<PlaybackControls
isPlaying={isPlaying}
isPaused={!isPlaying}
currentTime={currentTime}
duration={duration}
volume={masterVolume}
onPlay={play}
onPause={pause}
onStop={stop}
onSeek={seek}
onVolumeChange={setMasterVolume}
currentTimeFormatted={formatDuration(currentTime)}
durationFormatted={formatDuration(duration)}
isRecording={recordingState.isRecording}
onStartRecording={handleStartRecording}
onStopRecording={handleStopRecording}
punchInEnabled={punchInEnabled}
punchInTime={punchInTime}
punchOutTime={punchOutTime}
onPunchInEnabledChange={setPunchInEnabled}
onPunchInTimeChange={setPunchInTime}
onPunchOutTimeChange={setPunchOutTime}
overdubEnabled={overdubEnabled}
onOverdubEnabledChange={setOverdubEnabled}
/>
</div>
</div>
{/* Import Track Dialog */}
<ImportTrackDialog
open={importDialogOpen}
onClose={() => setImportDialogOpen(false)}
onImportTrack={handleImportTrack}
/>
<React.Suspense fallback={null}>
<ImportTrackDialog
open={importDialogOpen}
onClose={() => setImportDialogOpen(false)}
onImportTrack={handleImportTrack}
/>
</React.Suspense>
{/* Global Settings Dialog */}
<GlobalSettingsDialog
open={settingsDialogOpen}
onClose={() => setSettingsDialogOpen(false)}
recordingSettings={recordingSettings}
onInputGainChange={setInputGain}
onRecordMonoChange={setRecordMono}
onSampleRateChange={setSampleRate}
settings={settings}
onAudioSettingsChange={updateAudioSettings}
onUISettingsChange={updateUISettings}
onEditorSettingsChange={updateEditorSettings}
onPerformanceSettingsChange={updatePerformanceSettings}
onResetCategory={resetCategory}
/>
<React.Suspense fallback={null}>
<GlobalSettingsDialog
open={settingsDialogOpen}
onClose={() => setSettingsDialogOpen(false)}
recordingSettings={recordingSettings}
onInputGainChange={setInputGain}
onRecordMonoChange={setRecordMono}
onSampleRateChange={setSampleRate}
settings={settings}
onAudioSettingsChange={updateAudioSettings}
onUISettingsChange={updateUISettings}
onEditorSettingsChange={updateEditorSettings}
onPerformanceSettingsChange={updatePerformanceSettings}
onResetCategory={resetCategory}
/>
</React.Suspense>
{/* Export Dialog */}
<ExportDialog
open={exportDialogOpen}
onClose={() => setExportDialogOpen(false)}
onExport={handleExport}
isExporting={isExporting}
hasSelection={tracks.some(t => t.selection !== null)}
/>
<React.Suspense fallback={null}>
<ExportDialog
open={exportDialogOpen}
onClose={() => setExportDialogOpen(false)}
onExport={handleExport}
isExporting={isExporting}
hasSelection={tracks.some(t => t.selection !== null)}
/>
</React.Suspense>
{/* Projects Dialog */}
<ProjectsDialog
open={projectsDialogOpen}
onClose={() => setProjectsDialogOpen(false)}
projects={projects}
onNewProject={handleNewProject}
onLoadProject={handleLoadProject}
onDeleteProject={handleDeleteProject}
onDuplicateProject={handleDuplicateProject}
onExportProject={handleExportProject}
onImportProject={handleImportProject}
<React.Suspense fallback={null}>
<ProjectsDialog
open={projectsDialogOpen}
onClose={() => setProjectsDialogOpen(false)}
projects={projects}
onNewProject={handleNewProject}
onLoadProject={handleLoadProject}
onDeleteProject={handleDeleteProject}
onDuplicateProject={handleDuplicateProject}
onExportProject={handleExportProject}
onImportProject={handleImportProject}
/>
</React.Suspense>
{/* Browser Compatibility Dialog */}
<BrowserCompatDialog
open={browserCompatDialogOpen}
missingFeatures={browserCompatInfo.missingFeatures}
warnings={browserCompatInfo.warnings}
onClose={() => setBrowserCompatDialogOpen(false)}
/>
{/* Keyboard Shortcuts Dialog */}
<React.Suspense fallback={null}>
<KeyboardShortcutsDialog
open={shortcutsDialogOpen}
onClose={() => setShortcutsDialogOpen(false)}
/>
</React.Suspense>
</>
);
}