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
@@ -0,0 +1,40 @@
import { ref, onMounted, onUnmounted } from 'vue'
/**
* Tracks the visual viewport to detect mobile keyboard open/close.
* Uses the VisualViewport API to compute keyboard height as the difference
* between window.innerHeight and visualViewport.height.
*/
export function useVisualViewport() {
const keyboardHeight = ref(0)
const isKeyboardOpen = ref(false)
const viewportHeight = ref(typeof window !== 'undefined' ? window.innerHeight : 0)
function onViewportChange() {
const vv = window.visualViewport
if (!vv) return
const kbHeight = Math.max(0, window.innerHeight - vv.height)
keyboardHeight.value = kbHeight
isKeyboardOpen.value = kbHeight > 100
viewportHeight.value = vv.height
}
onMounted(() => {
const vv = window.visualViewport
if (vv) {
vv.addEventListener('resize', onViewportChange)
vv.addEventListener('scroll', onViewportChange)
onViewportChange()
}
})
onUnmounted(() => {
const vv = window.visualViewport
if (vv) {
vv.removeEventListener('resize', onViewportChange)
vv.removeEventListener('scroll', onViewportChange)
}
})
return { keyboardHeight, isKeyboardOpen, viewportHeight }
}