feat(app): mobile improvements — native-feel viewport, keyboard, HIG tabs, PWA fix
- Lock viewport: position:fixed on html/body prevents iOS bounce scroll - No zoom on input focus: maximum-scale=1, user-scalable=no - Keyboard-responsive chat: visualViewport API detects keyboard, hides tab bar, scrolls chat to bottom, scrollIntoView on input focus - iOS HIG tab bar: 49pt height, vertical icon+label, safe-area-inset-bottom - PWA fix: manifest start_url/scope/id changed from '/' to './' for subpath deployment compatibility - PlayerBar: variant prop (fixed/inline), inline on mobile above tab bar, fixed on desktop. No more overlap with tab bar. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
799ca131d9
commit
a0a9cf8e54
@@ -306,55 +306,62 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Bottom tab bar -->
|
||||
<!-- Inline player bar on mobile (above tab bar) -->
|
||||
<PlayerBar variant="inline" />
|
||||
|
||||
<!-- Bottom tab bar (iOS HIG: 49pt + safe area) -->
|
||||
<div
|
||||
class="shrink-0 flex items-center px-2 py-1.5 gap-1"
|
||||
v-show="!isKeyboardOpen"
|
||||
class="shrink-0"
|
||||
:style="{ paddingBottom: 'env(safe-area-inset-bottom, 0px)' }"
|
||||
>
|
||||
<button
|
||||
class="flex-1 flex items-center justify-center gap-1.5 py-2 rounded-xl text-xs font-medium transition-all duration-150"
|
||||
:class="mobileTab === 'chat'
|
||||
? isDark ? 'bg-white/10 text-white/90' : 'bg-black/8 text-gray-900'
|
||||
: isDark ? 'text-white/40 hover:text-white/60' : 'text-gray-400 hover:text-gray-600'"
|
||||
@click="mobileTab = 'chat'"
|
||||
>
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z" />
|
||||
</svg>
|
||||
Chat
|
||||
</button>
|
||||
<button
|
||||
class="flex-1 flex items-center justify-center gap-1.5 py-2 rounded-xl text-xs font-medium transition-all duration-150 relative"
|
||||
:class="mobileTab === 'content'
|
||||
? isDark ? 'bg-white/10 text-white/90' : 'bg-black/8 text-gray-900'
|
||||
: isDark ? 'text-white/40 hover:text-white/60' : 'text-gray-400 hover:text-gray-600'"
|
||||
@click="mobileTab = 'content'"
|
||||
>
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10" />
|
||||
</svg>
|
||||
Content
|
||||
<span
|
||||
v-if="panelOpen && mobileTab === 'chat'"
|
||||
class="absolute top-1 right-[30%] w-1.5 h-1.5 rounded-full bg-accent"
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
class="flex-1 flex items-center justify-center gap-1.5 py-2 rounded-xl text-xs font-medium transition-all duration-150 relative"
|
||||
:class="mobileTab === 'context'
|
||||
? isDark ? 'bg-white/10 text-white/90' : 'bg-black/8 text-gray-900'
|
||||
: isDark ? 'text-white/40 hover:text-white/60' : 'text-gray-400 hover:text-gray-600'"
|
||||
@click="mobileTab = 'context'"
|
||||
>
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
|
||||
</svg>
|
||||
Context
|
||||
<span
|
||||
v-if="hasDetailOpen && mobileTab !== 'context'"
|
||||
class="absolute top-1 right-[30%] w-1.5 h-1.5 rounded-full bg-accent"
|
||||
/>
|
||||
</button>
|
||||
<div class="flex items-center h-[49px] px-2 gap-1">
|
||||
<button
|
||||
class="flex-1 flex flex-col items-center justify-center h-[49px] min-h-[44px] rounded-xl text-[10px] font-medium tracking-wide transition-all duration-150 gap-0.5"
|
||||
:class="mobileTab === 'chat'
|
||||
? isDark ? 'bg-white/10 text-white/90' : 'bg-black/8 text-gray-900'
|
||||
: isDark ? 'text-white/40 hover:text-white/60' : 'text-gray-400 hover:text-gray-600'"
|
||||
@click="mobileTab = 'chat'"
|
||||
>
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z" />
|
||||
</svg>
|
||||
<span>Chat</span>
|
||||
</button>
|
||||
<button
|
||||
class="flex-1 flex flex-col items-center justify-center h-[49px] min-h-[44px] rounded-xl text-[10px] font-medium tracking-wide transition-all duration-150 relative gap-0.5"
|
||||
:class="mobileTab === 'content'
|
||||
? isDark ? 'bg-white/10 text-white/90' : 'bg-black/8 text-gray-900'
|
||||
: isDark ? 'text-white/40 hover:text-white/60' : 'text-gray-400 hover:text-gray-600'"
|
||||
@click="mobileTab = 'content'"
|
||||
>
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10" />
|
||||
</svg>
|
||||
<span>Content</span>
|
||||
<span
|
||||
v-if="panelOpen && mobileTab === 'chat'"
|
||||
class="absolute top-1.5 right-[28%] w-1.5 h-1.5 rounded-full bg-accent"
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
class="flex-1 flex flex-col items-center justify-center h-[49px] min-h-[44px] rounded-xl text-[10px] font-medium tracking-wide transition-all duration-150 relative gap-0.5"
|
||||
:class="mobileTab === 'context'
|
||||
? isDark ? 'bg-white/10 text-white/90' : 'bg-black/8 text-gray-900'
|
||||
: isDark ? 'text-white/40 hover:text-white/60' : 'text-gray-400 hover:text-gray-600'"
|
||||
@click="mobileTab = 'context'"
|
||||
>
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
|
||||
</svg>
|
||||
<span>Context</span>
|
||||
<span
|
||||
v-if="hasDetailOpen && mobileTab !== 'context'"
|
||||
class="absolute top-1.5 right-[28%] w-1.5 h-1.5 rounded-full bg-accent"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -367,12 +374,14 @@ import { useChatStore } from '@/stores/chat'
|
||||
import { useTheme } from '@/composables/useTheme'
|
||||
import { useAI } from '@/composables/useAI'
|
||||
import { useContentPanel, type ContentTab } from '@/composables/useContentPanel'
|
||||
import { useVisualViewport } from '@/composables/useVisualViewport'
|
||||
import ChatWindow from '@/components/chat/ChatWindow.vue'
|
||||
import ContentGridView from '@/components/content/ContentGridView.vue'
|
||||
import DetailView from '@/components/content/DetailView.vue'
|
||||
import CloseButton from '@/components/content/CloseButton.vue'
|
||||
import ContextLoader from '@/components/content/ContextLoader.vue'
|
||||
import ErrorBoundary from '@/components/ui/ErrorBoundary.vue'
|
||||
import PlayerBar from '@/components/player/PlayerBar.vue'
|
||||
import { useCodeContext } from '@/composables/useCodeContext'
|
||||
|
||||
const chatStore = useChatStore()
|
||||
@@ -446,6 +455,7 @@ const windowWidth = ref(window.innerWidth)
|
||||
const isMobile = computed(() => windowWidth.value < 1024)
|
||||
const isWideDesktop = computed(() => windowWidth.value >= 1440)
|
||||
const mobileTab = ref<'chat' | 'content' | 'context'>('chat')
|
||||
const { isKeyboardOpen } = useVisualViewport()
|
||||
|
||||
function onResize() { windowWidth.value = window.innerWidth }
|
||||
|
||||
|
||||
Reference in New Issue
Block a user