fix: iOS PWA status bar overlap issue on mobile devices

- Add PWA detection logic to App.jsx
- Apply dynamic padding to header and sidebar in PWA mode
- Update viewport meta tag for better iOS compatibility
- Change status bar style to black-translucent for PWA
- Add CSS variables for safe area insets with fallback support

This ensures menu button and sidebar content are properly visible
below the iOS status bar when installed as a PWA.
This commit is contained in:
Takumi Mori
2025-09-01 00:55:13 +09:00
parent e5709d71e0
commit 975e4b04a6
5 changed files with 125 additions and 12 deletions

View File

@@ -4,7 +4,7 @@
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<link rel="icon" type="image/png" href="/favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover" />
<meta name="viewport" content="initial-scale=1, viewport-fit=cover, width=device-width" />
<title>Claude Code UI</title>
<!-- PWA Manifest -->
@@ -12,7 +12,7 @@
<!-- iOS Safari PWA Meta Tags -->
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<meta name="apple-mobile-web-app-title" content="Claude UI" />
<!-- iOS Safari Icons -->

View File

@@ -78,6 +78,35 @@ function AppContent() {
const { ws, sendMessage, messages } = useWebSocketContext();
// Detect if running as PWA
const [isPWA, setIsPWA] = useState(false);
useEffect(() => {
// Check if running in standalone mode (PWA)
const checkPWA = () => {
const isStandalone = window.matchMedia('(display-mode: standalone)').matches ||
window.navigator.standalone ||
document.referrer.includes('android-app://');
setIsPWA(isStandalone);
// Add class to body for CSS targeting
if (isStandalone) {
document.body.classList.add('pwa-mode');
} else {
document.body.classList.remove('pwa-mode');
}
};
checkPWA();
// Listen for changes
window.matchMedia('(display-mode: standalone)').addEventListener('change', checkPWA);
return () => {
window.matchMedia('(display-mode: standalone)').removeEventListener('change', checkPWA);
};
}, []);
useEffect(() => {
const checkMobile = () => {
setIsMobile(window.innerWidth < 768);
@@ -561,6 +590,8 @@ function AppContent() {
latestVersion={latestVersion}
currentVersion={currentVersion}
onShowVersionModal={() => setShowVersionModal(true)}
isPWA={isPWA}
isMobile={isMobile}
/>
</div>
</div>
@@ -606,6 +637,8 @@ function AppContent() {
latestVersion={latestVersion}
currentVersion={currentVersion}
onShowVersionModal={() => setShowVersionModal(true)}
isPWA={isPWA}
isMobile={isMobile}
/>
</div>
</div>
@@ -622,6 +655,7 @@ function AppContent() {
sendMessage={sendMessage}
messages={messages}
isMobile={isMobile}
isPWA={isPWA}
onMenuClick={() => setSidebarOpen(true)}
isLoading={isLoadingProjects}
onInputFocusChange={setIsInputFocused}

View File

@@ -37,6 +37,7 @@ function MainContent({
sendMessage,
messages,
isMobile,
isPWA,
onMenuClick,
isLoading,
onInputFocusChange,
@@ -152,10 +153,13 @@ function MainContent({
<div className="h-full flex flex-col">
{/* Header with menu button for mobile */}
{isMobile && (
<div className="bg-white dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700 p-3 sm:p-4 flex-shrink-0">
<div
className="bg-white dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700 p-3 sm:p-4 pwa-header-safe flex-shrink-0"
style={isPWA && isMobile ? { paddingTop: '56px' } : {}}
>
<button
onClick={onMenuClick}
className="p-1.5 text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white rounded-md hover:bg-gray-100 dark:hover:bg-gray-700"
className="p-1.5 text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white rounded-md hover:bg-gray-100 dark:hover:bg-gray-700 pwa-menu-button"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" />
@@ -188,10 +192,13 @@ function MainContent({
<div className="h-full flex flex-col">
{/* Header with menu button for mobile */}
{isMobile && (
<div className="bg-white dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700 p-3 sm:p-4 flex-shrink-0">
<div
className="bg-white dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700 p-3 sm:p-4 pwa-header-safe flex-shrink-0"
style={isPWA && isMobile ? { paddingTop: '56px' } : {}}
>
<button
onClick={onMenuClick}
className="p-1.5 text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white rounded-md hover:bg-gray-100 dark:hover:bg-gray-700"
className="p-1.5 text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white rounded-md hover:bg-gray-100 dark:hover:bg-gray-700 pwa-menu-button"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" />
@@ -224,7 +231,10 @@ function MainContent({
return (
<div className="h-full flex flex-col">
{/* Header with tabs */}
<div className="bg-white dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700 p-3 sm:p-4 flex-shrink-0">
<div
className="bg-white dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700 p-3 sm:p-4 pwa-header-safe flex-shrink-0"
style={isPWA && isMobile ? { paddingTop: '56px' } : {}}
>
<div className="flex items-center justify-between">
<div className="flex items-center space-x-2 sm:space-x-3">
{isMobile && (
@@ -234,7 +244,7 @@ function MainContent({
e.preventDefault();
onMenuClick();
}}
className="p-2.5 text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white rounded-md hover:bg-gray-100 dark:hover:bg-gray-700 touch-manipulation active:scale-95"
className="p-2.5 text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white rounded-md hover:bg-gray-100 dark:hover:bg-gray-700 touch-manipulation active:scale-95 pwa-menu-button"
>
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" />

View File

@@ -54,7 +54,9 @@ function Sidebar({
updateAvailable,
latestVersion,
currentVersion,
onShowVersionModal
onShowVersionModal,
isPWA,
isMobile
}) {
const [expandedProjects, setExpandedProjects] = useState(new Set());
const [editingProject, setEditingProject] = useState(null);
@@ -434,7 +436,10 @@ function Sidebar({
};
return (
<div className="h-full flex flex-col bg-card md:select-none">
<div
className="h-full flex flex-col bg-card md:select-none"
style={isPWA && isMobile ? { paddingTop: '44px' } : {}}
>
{/* Header */}
<div className="md:p-4 md:border-b md:border-border">
{/* Desktop Header */}
@@ -479,7 +484,10 @@ function Sidebar({
</div>
{/* Mobile Header */}
<div className="md:hidden p-3 border-b border-border">
<div
className="md:hidden p-3 border-b border-border"
style={isPWA && isMobile ? { paddingTop: '16px' } : {}}
>
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="w-8 h-8 bg-primary rounded-lg flex items-center justify-center">

View File

@@ -43,6 +43,22 @@
--input: 214.3 31.8% 91.4%;
--ring: 221.2 83.2% 53.3%;
--radius: 0.5rem;
/* Safe area CSS variables */
--safe-area-inset-top: env(safe-area-inset-top);
--safe-area-inset-right: env(safe-area-inset-right);
--safe-area-inset-bottom: env(safe-area-inset-bottom);
--safe-area-inset-left: env(safe-area-inset-left);
}
/* Fallback for older iOS versions */
@supports (padding-top: constant(safe-area-inset-top)) {
:root {
--safe-area-inset-top: constant(safe-area-inset-top);
--safe-area-inset-right: constant(safe-area-inset-right);
--safe-area-inset-bottom: constant(safe-area-inset-bottom);
--safe-area-inset-left: constant(safe-area-inset-left);
}
}
.dark {
@@ -82,12 +98,46 @@
padding: 0;
}
html, body, #root {
html, body {
height: 100%;
margin: 0;
padding: 0;
}
/* Root element with safe area padding for PWA */
#root {
min-height: 100vh;
margin: 0;
padding: 0;
box-sizing: border-box;
}
/* Apply safe area padding in standalone mode */
@supports (padding-top: env(safe-area-inset-top)) {
@media (display-mode: standalone) {
#root {
padding-top: var(--safe-area-inset-top);
padding-left: var(--safe-area-inset-left);
padding-right: var(--safe-area-inset-right);
}
}
}
/* PWA mode detected by JavaScript - more reliable */
body.pwa-mode #root {
padding-top: calc(env(safe-area-inset-top, 0px) + 20px);
padding-left: env(safe-area-inset-left);
padding-right: env(safe-area-inset-right);
}
/* iOS specific PWA adjustments */
@supports (-webkit-touch-callout: none) {
body.pwa-mode #root {
/* iOS status bar is typically 20-44px */
padding-top: max(env(safe-area-inset-top, 44px), 44px);
}
}
/* Global transition defaults */
button,
a,
@@ -577,6 +627,17 @@
padding-bottom: max(env(safe-area-inset-bottom), 12px);
}
/* PWA specific header adjustments for iOS */
.pwa-header-safe {
padding-top: 16px;
}
/* When PWA mode is detected by JavaScript */
body.pwa-mode .pwa-header-safe {
/* Reset padding since #root already handles safe area */
padding-top: 12px;
}
@media screen and (max-width: 768px) {
.chat-input-mobile {
padding-bottom: calc(60px + max(env(safe-area-inset-bottom), 12px));