/** * Mobile Safari Detection and Optimization Utilities * Handles Safari-specific issues and performance optimizations */ export interface DeviceInfo { isMobile: boolean; isSafari: boolean; isMobileSafari: boolean; isIOS: boolean; safariVersion?: number; } /** * Detect device and browser information */ export function getDeviceInfo(): DeviceInfo { if (typeof window === 'undefined') { return { isMobile: false, isSafari: false, isMobileSafari: false, isIOS: false }; } const userAgent = navigator.userAgent; const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(userAgent); const isIOS = /iPad|iPhone|iPod/.test(userAgent); const isSafari = /^((?!chrome|android).)*safari/i.test(userAgent); const isMobileSafari = isIOS && isSafari; // Extract Safari version if possible let safariVersion: number | undefined; if (isSafari) { const match = userAgent.match(/Version\/(\d+)/); if (match) { safariVersion = parseInt(match[1]); } } return { isMobile, isSafari, isMobileSafari, isIOS, safariVersion }; } /** * Check if the device needs performance optimizations */ export function needsPerformanceOptimization(): boolean { const { isMobileSafari, isMobile } = getDeviceInfo(); return isMobileSafari || isMobile; } /** * Check if backdrop-filter should be disabled */ export function shouldDisableBackdropFilter(): boolean { const { isMobileSafari, safariVersion } = getDeviceInfo(); // Disable backdrop-filter on mobile Safari or older Safari versions return isMobileSafari || Boolean(safariVersion && safariVersion < 16); } /** * Get optimized CSS class names based on device */ export function getOptimizedClasses(): string[] { const classes: string[] = []; const { isMobile, isMobileSafari, isIOS } = getDeviceInfo(); if (isMobile) classes.push('is-mobile'); if (isMobileSafari) classes.push('is-mobile-safari'); if (isIOS) classes.push('is-ios'); if (needsPerformanceOptimization()) classes.push('performance-mode'); return classes; } /** * Optimized viewport height for mobile Safari */ export function getOptimizedViewportHeight(): string { const { isMobileSafari, safariVersion } = getDeviceInfo(); if (isMobileSafari) { // Use 100vh for older Safari, -webkit-fill-available for newer return safariVersion && safariVersion >= 15 ? '-webkit-fill-available' : '100vh'; } // Use dvh for modern browsers, vh as fallback return '100vh'; } /** * Apply mobile Safari specific fixes */ export function applyMobileSafariFixes(): void { if (typeof window === 'undefined') return; const { isMobileSafari } = getDeviceInfo(); if (!isMobileSafari) return; // Fix viewport height issues const setViewportHeight = () => { const vh = window.innerHeight * 0.01; document.documentElement.style.setProperty('--vh', `${vh}px`); }; // Set initial value setViewportHeight(); // Update on resize (debounced) let resizeTimeout: NodeJS.Timeout; window.addEventListener('resize', () => { clearTimeout(resizeTimeout); resizeTimeout = setTimeout(setViewportHeight, 100); }); // Add performance optimization classes document.documentElement.classList.add(...getOptimizedClasses()); } /** * Throttle function for performance */ export function throttle void>( func: T, wait: number ): (...args: Parameters) => void { let timeout: NodeJS.Timeout | null = null; let previous = 0; return function(this: any, ...args: Parameters) { const now = Date.now(); const remaining = wait - (now - previous); if (remaining <= 0 || remaining > wait) { if (timeout) { clearTimeout(timeout); timeout = null; } previous = now; func.apply(this, args); } else if (!timeout) { timeout = setTimeout(() => { previous = Date.now(); timeout = null; func.apply(this, args); }, remaining); } }; } /** * Debounce function for performance */ export function debounce void>( func: T, wait: number ): (...args: Parameters) => void { let timeout: NodeJS.Timeout; return function(this: any, ...args: Parameters) { clearTimeout(timeout); timeout = setTimeout(() => func.apply(this, args), wait); }; }