(), { variant: 'fixed' })
+
const {
currentSong,
hasTrack,
diff --git a/packages/app/src/composables/useVisualViewport.ts b/packages/app/src/composables/useVisualViewport.ts
new file mode 100644
index 00000000..0acda87e
--- /dev/null
+++ b/packages/app/src/composables/useVisualViewport.ts
@@ -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 }
+}
diff --git a/packages/app/src/pages/ChatPage.vue b/packages/app/src/pages/ChatPage.vue
index 3e0e0f2a..d60a75a0 100644
--- a/packages/app/src/pages/ChatPage.vue
+++ b/packages/app/src/pages/ChatPage.vue
@@ -306,55 +306,62 @@
-
+
+