From f3c54d4560f078349ddbb18053247f8d28d80ab8 Mon Sep 17 00:00:00 2001 From: Andrei Date: Fri, 10 Oct 2025 12:41:32 +0000 Subject: [PATCH] feat: add mobile gesture navigation system MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implemented comprehensive mobile gesture support for the Bible reader: **Swipe Gestures:** - Swipe left/right to navigate between chapters - Only activates on mobile devices (touch events) - Configurable 50px minimum swipe distance - Prevents scrolling interference **Tap Zones:** - Left 25% of screen: navigate to previous chapter - Right 25% of screen: navigate to next chapter - Center 50%: normal reading interaction - Maintains text selection capabilities **Smooth Page Transitions:** - Fade and scale animation on chapter navigation - 300ms duration with ease-in-out timing - Visual feedback: opacity 0.5 and scale 0.98 during transition - Applied to all navigation methods (swipe, tap, keyboard, buttons) **Settings Controls:** - Enable/disable swipe gestures toggle - Enable/disable tap zones toggle - Pagination mode toggle (for future enhancement) - All settings persist in localStorage **Dependencies:** - Added react-swipeable v7.0.2 for gesture handling - Zero-dependency, lightweight (peer deps: React only) **User Experience:** - Settings grouped under "Mobile Navigation" section - Default enabled for optimal mobile UX - Touch-optimized for tablets and phones - Desktop users can disable if desired This completes all mobile navigation features from Phase 1. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- app/[locale]/bible/reader.tsx | 105 +++++++++++++++++++++++++++++++++- package-lock.json | 10 ++++ package.json | 1 + 3 files changed, 113 insertions(+), 3 deletions(-) diff --git a/app/[locale]/bible/reader.tsx b/app/[locale]/bible/reader.tsx index de8c430..fb2c248 100644 --- a/app/[locale]/bible/reader.tsx +++ b/app/[locale]/bible/reader.tsx @@ -8,6 +8,7 @@ import { OfflineDownloadManager } from '@/components/bible/offline-download-mana import { OfflineBibleReader } from '@/components/bible/offline-bible-reader' import { offlineStorage } from '@/lib/offline-storage' import { InstallPrompt, useInstallPrompt } from '@/components/pwa/install-prompt' +import { useSwipeable } from 'react-swipeable' import { Box, Typography, @@ -137,6 +138,9 @@ interface ReadingPreferences { wordSpacing: number // 0-4px range for word spacing paragraphSpacing: number // 1.0-2.5x line height for paragraph spacing maxLineLength: number // 50-100 characters (ch units) for optimal reading width + enableSwipeGestures: boolean // Enable swipe left/right for chapter navigation + enableTapZones: boolean // Enable tap zones (left=prev, right=next) + paginationMode: boolean // Page-by-page vs continuous scroll } const defaultPreferences: ReadingPreferences = { @@ -150,7 +154,10 @@ const defaultPreferences: ReadingPreferences = { letterSpacing: 0.5, // 0.5px default (WCAG 2.1 SC 1.4.12 recommends 0.12em) wordSpacing: 0, // 0px default (browser default is optimal) paragraphSpacing: 1.8, // 1.8x line spacing (WCAG recommends ≥1.5x) - maxLineLength: 75 // 75ch optimal reading width (50-75 for desktop) + maxLineLength: 75, // 75ch optimal reading width (50-75 for desktop) + enableSwipeGestures: true, // Enable by default for mobile + enableTapZones: true, // Enable by default for mobile + paginationMode: false // Continuous scroll by default } interface BibleReaderProps { @@ -238,6 +245,9 @@ export default function BibleReaderNew({ initialVersion, initialBook, initialCha const [readingProgress, setReadingProgress] = useState(null) const [hasLoadedInitialProgress, setHasLoadedInitialProgress] = useState(false) + // Page transition state + const [isTransitioning, setIsTransitioning] = useState(false) + // Note dialog state const [noteDialog, setNoteDialog] = useState<{ open: boolean @@ -944,6 +954,10 @@ export default function BibleReaderNew({ initialVersion, initialBook, initialCha } const handlePreviousChapter = () => { + // Trigger transition animation + setIsTransitioning(true) + setTimeout(() => setIsTransitioning(false), 300) // Match CSS transition duration + if (selectedChapter > 1) { const newChapter = selectedChapter - 1 setSelectedChapter(newChapter) @@ -961,6 +975,10 @@ export default function BibleReaderNew({ initialVersion, initialBook, initialCha } const handleNextChapter = () => { + // Trigger transition animation + setIsTransitioning(true) + setTimeout(() => setIsTransitioning(false), 300) // Match CSS transition duration + if (selectedChapter < maxChapters) { const newChapter = selectedChapter + 1 setSelectedChapter(newChapter) @@ -976,6 +994,41 @@ export default function BibleReaderNew({ initialVersion, initialBook, initialCha } } + // Swipe handlers for mobile navigation + const swipeHandlers = useSwipeable({ + onSwipedLeft: () => { + if (preferences.enableSwipeGestures && isMobile) { + handleNextChapter() + } + }, + onSwipedRight: () => { + if (preferences.enableSwipeGestures && isMobile) { + handlePreviousChapter() + } + }, + preventScrollOnSwipe: false, + trackMouse: false, // Only track touch, not mouse + delta: 50 // Minimum swipe distance in pixels + }) + + // Tap zone handler for quick navigation + const handleTapZone = (event: React.MouseEvent) => { + if (!preferences.enableTapZones || !isMobile) return + + const target = event.currentTarget + const rect = target.getBoundingClientRect() + const clickX = event.clientX - rect.left + const tapZoneWidth = rect.width * 0.25 // 25% on each side + + if (clickX < tapZoneWidth) { + // Left tap zone - previous chapter + handlePreviousChapter() + } else if (clickX > rect.width - tapZoneWidth) { + // Right tap zone - next chapter + handleNextChapter() + } + } + const handleChapterBookmark = async () => { if (!selectedBook || !selectedChapter) return @@ -1993,6 +2046,41 @@ export default function BibleReaderNew({ initialVersion, initialBook, initialCha } label={t('readingMode')} /> + + + + Mobile Navigation + + + setPreferences(prev => ({ ...prev, enableSwipeGestures: e.target.checked }))} + /> + } + label="Enable Swipe Gestures" + /> + + setPreferences(prev => ({ ...prev, enableTapZones: e.target.checked }))} + /> + } + label="Enable Tap Zones" + /> + + setPreferences(prev => ({ ...prev, paginationMode: e.target.checked }))} + /> + } + label="Pagination Mode" + /> @@ -2058,13 +2146,18 @@ export default function BibleReaderNew({ initialVersion, initialBook, initialCha {/* Reading Content */} {loading && ( diff --git a/package-lock.json b/package-lock.json index 7916e39..7fd924f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -63,6 +63,7 @@ "react": "^19.1.1", "react-dom": "^19.1.1", "react-markdown": "^10.1.0", + "react-swipeable": "^7.0.2", "recharts": "^3.2.1", "remark-gfm": "^4.0.1", "socket.io": "^4.8.1", @@ -7422,6 +7423,15 @@ } } }, + "node_modules/react-swipeable": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/react-swipeable/-/react-swipeable-7.0.2.tgz", + "integrity": "sha512-v1Qx1l+aC2fdxKa9aKJiaU/ZxmJ5o98RMoFwUqAAzVWUcxgfHFXDDruCKXhw6zIYXm6V64JiHgP9f6mlME5l8w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.3 || ^17 || ^18 || ^19.0.0 || ^19.0.0-rc" + } + }, "node_modules/react-transition-group": { "version": "4.4.5", "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", diff --git a/package.json b/package.json index 1505a9d..7d721df 100644 --- a/package.json +++ b/package.json @@ -76,6 +76,7 @@ "react": "^19.1.1", "react-dom": "^19.1.1", "react-markdown": "^10.1.0", + "react-swipeable": "^7.0.2", "recharts": "^3.2.1", "remark-gfm": "^4.0.1", "socket.io": "^4.8.1",