diff --git a/capacitor.config.ts b/capacitor.config.ts index 7692892..8ff0492 100644 --- a/capacitor.config.ts +++ b/capacitor.config.ts @@ -10,6 +10,19 @@ const config: CapacitorConfig = { androidScheme: 'https', }, plugins: { + SplashScreen: { + launchShowDuration: 2000, + backgroundColor: '#FFFFFF', + androidScaleType: 'CENTER_CROP', + showSpinner: false, + splashFullScreen: true, + splashImmersive: true, + }, + Keyboard: { + // 'native' resize mode on iOS avoids pushing the whole webview up + resize: 'native', + resizeOnFullScreen: true, + }, // No special config needed for Network plugin - it works out of the box }, // iOS: allow offline usage and background fetch diff --git a/src/index.css b/src/index.css index dddcb96..f453e33 100644 --- a/src/index.css +++ b/src/index.css @@ -15,6 +15,22 @@ body { padding-top: var(--safe-area-top); padding-bottom: var(--safe-area-bottom); + /* Prevent overscroll bounce on iOS — keeps app feeling native */ + overscroll-behavior: none; + /* Use dvh for proper mobile viewport (accounts for browser chrome) */ + min-height: 100dvh; +} + +/* Prevent iOS Safari input zoom — inputs must be >= 16px */ +@supports (-webkit-touch-callout: none) { + input, select, textarea { + font-size: max(16px, 1em); + } +} + +/* Prevent pull-to-refresh in native WebView */ +html, body { + overscroll-behavior-y: none; } /* Custom animations */ @@ -229,6 +245,13 @@ html { scroll-behavior: smooth; } +/* Mobile viewport height fix — min-h-screen uses 100vh which is wrong on mobile. + Use this utility class where full-height layouts are needed. */ +.min-h-screen-safe { + min-height: 100vh; + min-height: 100dvh; +} + /* Safe area insets for mobile devices */ .safe-area-inset-top { padding-top: env(safe-area-inset-top, 0px); diff --git a/src/lib/native.ts b/src/lib/native.ts index e5aac7c..1a3b97c 100644 --- a/src/lib/native.ts +++ b/src/lib/native.ts @@ -1,24 +1,60 @@ import { Capacitor } from '@capacitor/core'; -export async function initNativePlatform() { +/** + * Update the native status bar style to match the current theme. + * Safe to call at any time — no-ops on web. + */ +export async function updateStatusBarStyle() { if (!Capacitor.isNativePlatform()) return; - + const { StatusBar, Style } = await import('@capacitor/status-bar'); - - // Set status bar style based on current theme const isDark = document.documentElement.classList.contains('dark'); await StatusBar.setStyle({ style: isDark ? Style.Dark : Style.Light }); - +} + +export async function initNativePlatform() { + if (!Capacitor.isNativePlatform()) return; + + // Set initial status bar style + await updateStatusBarStyle(); + // On Android, make status bar transparent for edge-to-edge if (Capacitor.getPlatform() === 'android') { + const { StatusBar } = await import('@capacitor/status-bar'); await StatusBar.setBackgroundColor({ color: '#00000000' }); await StatusBar.setOverlaysWebView({ overlay: true }); } + + // Watch for dark mode changes and sync status bar + const observer = new MutationObserver((mutations) => { + for (const mutation of mutations) { + if (mutation.attributeName === 'class') { + updateStatusBarStyle(); + } + } + }); + observer.observe(document.documentElement, { attributes: true, attributeFilter: ['class'] }); } export async function initKeyboardHandling() { if (!Capacitor.isNativePlatform()) return; - await import('@capacitor/keyboard'); - // Keyboard will push content up automatically in Capacitor - // Add any custom keyboard behavior here + + const { Keyboard } = await import('@capacitor/keyboard'); + + // On iOS, use resize mode that doesn't push the whole webview up + // This prevents janky layout shifts when the keyboard opens + if (Capacitor.getPlatform() === 'ios') { + await Keyboard.setAccessoryBarVisible({ isVisible: true }); + } + + // Scroll focused input into view when keyboard opens + Keyboard.addListener('keyboardWillShow', () => { + const activeEl = document.activeElement as HTMLElement | null; + if (activeEl && (activeEl.tagName === 'INPUT' || activeEl.tagName === 'TEXTAREA')) { + // Small delay to let keyboard animation start + setTimeout(() => { + activeEl.scrollIntoView({ behavior: 'smooth', block: 'center' }); + }, 100); + } + }); } diff --git a/src/main.tsx b/src/main.tsx index 2e0e2b7..de81a88 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -8,7 +8,7 @@ import { ToastProvider } from './hooks/useToast' import { SettingsProvider } from './hooks/useSettings' import { registerServiceWorker } from './lib/sw-registration' import { initDarkMode } from './lib/storage' -import { initNativePlatform } from './lib/native' +import { initNativePlatform, initKeyboardHandling } from './lib/native' import './index.css' import App from './App.tsx' @@ -19,6 +19,7 @@ initDarkMode(); // Initialize native platform features (status bar, keyboard) initNativePlatform(); +initKeyboardHandling(); // Register service worker for offline support (web only - disabled in native apps) if (!Capacitor.isNativePlatform()) {