Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions capacitor.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
23 changes: 23 additions & 0 deletions src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Expand Down Expand Up @@ -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);
Expand Down
52 changes: 44 additions & 8 deletions src/lib/native.ts
Original file line number Diff line number Diff line change
@@ -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);
}
});
}
3 changes: 2 additions & 1 deletion src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand All @@ -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()) {
Expand Down
Loading