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:
133
lib/utils/browser-compat.ts
Normal file
133
lib/utils/browser-compat.ts
Normal file
@@ -0,0 +1,133 @@
|
||||
/**
|
||||
* Browser compatibility checking utilities
|
||||
*/
|
||||
|
||||
export interface BrowserCompatibility {
|
||||
isSupported: boolean;
|
||||
missingFeatures: string[];
|
||||
warnings: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if all required browser features are supported
|
||||
*/
|
||||
export function checkBrowserCompatibility(): BrowserCompatibility {
|
||||
const missingFeatures: string[] = [];
|
||||
const warnings: string[] = [];
|
||||
|
||||
// Check if running in browser
|
||||
if (typeof window === 'undefined') {
|
||||
return {
|
||||
isSupported: true,
|
||||
missingFeatures: [],
|
||||
warnings: [],
|
||||
};
|
||||
}
|
||||
|
||||
// Check Web Audio API
|
||||
if (!window.AudioContext && !(window as any).webkitAudioContext) {
|
||||
missingFeatures.push('Web Audio API');
|
||||
}
|
||||
|
||||
// Check IndexedDB
|
||||
if (!window.indexedDB) {
|
||||
missingFeatures.push('IndexedDB');
|
||||
}
|
||||
|
||||
// Check localStorage
|
||||
try {
|
||||
localStorage.setItem('test', 'test');
|
||||
localStorage.removeItem('test');
|
||||
} catch (e) {
|
||||
missingFeatures.push('LocalStorage');
|
||||
}
|
||||
|
||||
// Check Canvas API
|
||||
const canvas = document.createElement('canvas');
|
||||
if (!canvas.getContext || !canvas.getContext('2d')) {
|
||||
missingFeatures.push('Canvas API');
|
||||
}
|
||||
|
||||
// Check MediaDevices API (for recording)
|
||||
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
|
||||
warnings.push('Microphone recording not supported (requires HTTPS or localhost)');
|
||||
}
|
||||
|
||||
// Check File API
|
||||
if (!window.File || !window.FileReader || !window.FileList || !window.Blob) {
|
||||
missingFeatures.push('File API');
|
||||
}
|
||||
|
||||
// Check AudioWorklet support (optional)
|
||||
if (window.AudioContext && !AudioContext.prototype.hasOwnProperty('audioWorklet')) {
|
||||
warnings.push('AudioWorklet not supported (some features may have higher latency)');
|
||||
}
|
||||
|
||||
// Check OfflineAudioContext
|
||||
if (!window.OfflineAudioContext && !(window as any).webkitOfflineAudioContext) {
|
||||
missingFeatures.push('OfflineAudioContext (required for audio processing)');
|
||||
}
|
||||
|
||||
return {
|
||||
isSupported: missingFeatures.length === 0,
|
||||
missingFeatures,
|
||||
warnings,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user-friendly browser name
|
||||
*/
|
||||
export function getBrowserInfo(): { name: string; version: string } {
|
||||
// Check if running in browser
|
||||
if (typeof window === 'undefined' || typeof navigator === 'undefined') {
|
||||
return { name: 'Unknown', version: 'Unknown' };
|
||||
}
|
||||
|
||||
const userAgent = navigator.userAgent;
|
||||
let name = 'Unknown';
|
||||
let version = 'Unknown';
|
||||
|
||||
if (userAgent.indexOf('Chrome') > -1 && userAgent.indexOf('Edg') === -1) {
|
||||
name = 'Chrome';
|
||||
const match = userAgent.match(/Chrome\/(\d+)/);
|
||||
version = match ? match[1] : 'Unknown';
|
||||
} else if (userAgent.indexOf('Edg') > -1) {
|
||||
name = 'Edge';
|
||||
const match = userAgent.match(/Edg\/(\d+)/);
|
||||
version = match ? match[1] : 'Unknown';
|
||||
} else if (userAgent.indexOf('Firefox') > -1) {
|
||||
name = 'Firefox';
|
||||
const match = userAgent.match(/Firefox\/(\d+)/);
|
||||
version = match ? match[1] : 'Unknown';
|
||||
} else if (userAgent.indexOf('Safari') > -1 && userAgent.indexOf('Chrome') === -1) {
|
||||
name = 'Safari';
|
||||
const match = userAgent.match(/Version\/(\d+)/);
|
||||
version = match ? match[1] : 'Unknown';
|
||||
}
|
||||
|
||||
return { name, version };
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if browser version meets minimum requirements
|
||||
*/
|
||||
export function checkMinimumVersion(): boolean {
|
||||
const { name, version } = getBrowserInfo();
|
||||
const versionNum = parseInt(version, 10);
|
||||
|
||||
const minimumVersions: Record<string, number> = {
|
||||
Chrome: 90,
|
||||
Edge: 90,
|
||||
Firefox: 88,
|
||||
Safari: 14,
|
||||
};
|
||||
|
||||
const minVersion = minimumVersions[name];
|
||||
if (!minVersion) {
|
||||
// Unknown browser, assume it's ok
|
||||
return true;
|
||||
}
|
||||
|
||||
return versionNum >= minVersion;
|
||||
}
|
||||
Reference in New Issue
Block a user