Files
archy/packages/app/src/composables/useSwipeNavigation.ts
T
DorianandClaude Opus 4.6 6be4fbe081 feat(mobile): mobile UX polish with bottom sheet, haptics, gestures, PWA (M16.1-M16.10)
- BottomSheet.vue: gesture-driven with 40/80/100% snap points, backdrop
- Swipe navigation: left/right to switch conversations, 80px threshold
- Pull-to-refresh: custom glass spinner, haptic on release
- Haptic feedback: useHaptics() composable with pattern presets
- Web Share API: native share with glass fallback sheet
- Pinch-to-zoom: usePinchZoom() composable, 1x-4x, double-tap reset
- iOS PWA: safe area insets CSS utilities, black-translucent status bar
- Long-press context menus: 500ms trigger via bottom sheet
- Scroll position memory: per-route Map, auto-restore on mount
- Landscape mode: CSS split layout, orientation change detection

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 00:55:19 +00:00

78 lines
1.9 KiB
TypeScript

import { ref, onMounted, onUnmounted } from 'vue'
export function useSwipeNavigation(options: {
onSwipeLeft?: () => void
onSwipeRight?: () => void
threshold?: number
velocityThreshold?: number
}) {
const { threshold = 80, velocityThreshold = 0.3 } = options
const isSwiping = ref(false)
const swipeOffset = ref(0)
let startX = 0
let startY = 0
let startTime = 0
let tracking = false
function onTouchStart(e: TouchEvent) {
startX = e.touches[0].clientX
startY = e.touches[0].clientY
startTime = Date.now()
tracking = true
swipeOffset.value = 0
}
function onTouchMove(e: TouchEvent) {
if (!tracking) return
const dx = e.touches[0].clientX - startX
const dy = e.touches[0].clientY - startY
// Only track horizontal swipes
if (Math.abs(dy) > Math.abs(dx)) {
tracking = false
return
}
swipeOffset.value = dx
isSwiping.value = Math.abs(dx) > 20
}
function onTouchEnd() {
if (!tracking) {
isSwiping.value = false
swipeOffset.value = 0
return
}
const elapsed = Date.now() - startTime
const velocity = Math.abs(swipeOffset.value) / elapsed
if (Math.abs(swipeOffset.value) > threshold || velocity > velocityThreshold) {
if (swipeOffset.value > 0) {
options.onSwipeRight?.()
} else {
options.onSwipeLeft?.()
}
}
tracking = false
isSwiping.value = false
swipeOffset.value = 0
}
onMounted(() => {
document.addEventListener('touchstart', onTouchStart, { passive: true })
document.addEventListener('touchmove', onTouchMove, { passive: true })
document.addEventListener('touchend', onTouchEnd)
})
onUnmounted(() => {
document.removeEventListener('touchstart', onTouchStart)
document.removeEventListener('touchmove', onTouchMove)
document.removeEventListener('touchend', onTouchEnd)
})
return { isSwiping, swipeOffset }
}