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:
Dorian
2026-03-04 20:50:06 +00:00
co-authored by Claude Opus 4.6
parent 799ca131d9
commit a0a9cf8e54
9 changed files with 142 additions and 57 deletions
@@ -106,7 +106,7 @@
@keydown="handleKeydown"
@input="autoResize"
@paste="onPaste"
@focus="focused = true"
@focus="onInputFocus"
@blur="focused = false"
/>
<button
@@ -252,6 +252,13 @@ function closePalette() {
}
}
function onInputFocus() {
focused.value = true
nextTick(() => {
textareaRef.value?.scrollIntoView({ block: 'end', behavior: 'smooth' })
})
}
function handleKeydown(e: KeyboardEvent) {
if (isPaletteMode.value && !paletteRef.value?.hasSelectedTemplate) {
if (e.key === 'ArrowUp') {
@@ -179,6 +179,7 @@ const chatStore = useChatStore()
const { sendMessage, stopGeneration, editAndResend, regenerateLastResponse, activeModel } = useAI()
const { updatePanelFromText, panelOpen, activeTab, availableTabs, setActiveTab, enterDesignSystemMode } = useContentPanel()
import { useCodeContext } from '@/composables/useCodeContext'
import { useVisualViewport } from '@/composables/useVisualViewport'
const codeContext = useCodeContext()
import { usePersonaStore } from '@/stores/personas'
const personaStore = usePersonaStore()
@@ -434,6 +435,12 @@ watch(chatCollapsed, (collapsed, wasCollapsed) => {
}
})
// Scroll to bottom when mobile keyboard opens so latest messages + input stay visible
const { isKeyboardOpen } = useVisualViewport()
watch(isKeyboardOpen, (open) => {
if (open) scrollToBottom()
})
// Save/restore scroll position on conversation switch
watch(
() => chatStore.activeConversationId,
@@ -2,7 +2,12 @@
<Transition name="player-slide">
<div
v-if="hasTrack"
class="fixed bottom-0 left-0 right-0 z-[999] flex items-center gap-4 px-4 py-3 path-glass-card rounded-none border-t-0 border-x-0 shadow-2xl"
:class="[
props.variant === 'fixed'
? 'fixed bottom-0 left-0 right-0 z-[999]'
: 'w-full shrink-0',
'flex items-center gap-4 px-4 py-3 path-glass-card rounded-none border-t-0 border-x-0 shadow-2xl'
]"
>
<!-- Plyr container: YouTube requires min 200x200px. Kept off-screen but sized. -->
<div
@@ -113,6 +118,10 @@ import { ref, computed, onMounted, watch, nextTick } from 'vue'
import { usePlayer } from '@/composables/usePlayer'
import { fetchMusicCover } from '@/composables/useImageFallback'
const props = withDefaults(defineProps<{
variant?: 'fixed' | 'inline'
}>(), { variant: 'fixed' })
const {
currentSong,
hasTrack,