|
|
|
|
@@ -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 {
|
|
|
|
|
@@ -168,6 +175,40 @@ export default function BibleReaderNew({ initialVersion, initialBook, initialCha
|
|
|
|
|
const searchParams = useSearchParams()
|
|
|
|
|
const { user } = useAuth()
|
|
|
|
|
|
|
|
|
|
// Add global accessibility styles for focus indicators (WCAG AAA)
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
const style = document.createElement('style')
|
|
|
|
|
style.innerHTML = `
|
|
|
|
|
/* Global focus indicators - WCAG AAA Compliance */
|
|
|
|
|
button:focus-visible,
|
|
|
|
|
a:focus-visible,
|
|
|
|
|
input:focus-visible,
|
|
|
|
|
textarea:focus-visible,
|
|
|
|
|
select:focus-visible,
|
|
|
|
|
[role="button"]:focus-visible,
|
|
|
|
|
[tabindex]:not([tabindex="-1"]):focus-visible {
|
|
|
|
|
outline: 2px solid #1976d2 !important;
|
|
|
|
|
outline-offset: 2px !important;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Ensure 200% zoom support - WCAG AAA */
|
|
|
|
|
@media (max-width: 1280px) {
|
|
|
|
|
html {
|
|
|
|
|
font-size: 100% !important;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Prevent horizontal scroll at 200% zoom */
|
|
|
|
|
body {
|
|
|
|
|
overflow-x: hidden;
|
|
|
|
|
}
|
|
|
|
|
`
|
|
|
|
|
document.head.appendChild(style)
|
|
|
|
|
return () => {
|
|
|
|
|
document.head.removeChild(style)
|
|
|
|
|
}
|
|
|
|
|
}, [])
|
|
|
|
|
|
|
|
|
|
// Use initial props if provided, otherwise use search params
|
|
|
|
|
const effectiveParams = React.useMemo(() => {
|
|
|
|
|
if (initialVersion || initialBook || initialChapter) {
|
|
|
|
|
@@ -238,6 +279,12 @@ export default function BibleReaderNew({ initialVersion, initialBook, initialCha
|
|
|
|
|
const [readingProgress, setReadingProgress] = useState<any>(null)
|
|
|
|
|
const [hasLoadedInitialProgress, setHasLoadedInitialProgress] = useState(false)
|
|
|
|
|
|
|
|
|
|
// Page transition state
|
|
|
|
|
const [isTransitioning, setIsTransitioning] = useState(false)
|
|
|
|
|
|
|
|
|
|
// Accessibility announcement state
|
|
|
|
|
const [ariaAnnouncement, setAriaAnnouncement] = useState('')
|
|
|
|
|
|
|
|
|
|
// Note dialog state
|
|
|
|
|
const [noteDialog, setNoteDialog] = useState<{
|
|
|
|
|
open: boolean
|
|
|
|
|
@@ -944,10 +991,16 @@ 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)
|
|
|
|
|
updateUrl(selectedBook, newChapter, selectedVersion)
|
|
|
|
|
// Announce for screen readers
|
|
|
|
|
setAriaAnnouncement(`Navigated to ${currentBook?.name} chapter ${newChapter}`)
|
|
|
|
|
} else {
|
|
|
|
|
const currentBookIndex = books.findIndex(book => book.id === selectedBook)
|
|
|
|
|
if (currentBookIndex > 0) {
|
|
|
|
|
@@ -956,15 +1009,23 @@ export default function BibleReaderNew({ initialVersion, initialBook, initialCha
|
|
|
|
|
setSelectedBook(previousBook.id)
|
|
|
|
|
setSelectedChapter(lastChapter)
|
|
|
|
|
updateUrl(previousBook.id, lastChapter, selectedVersion)
|
|
|
|
|
// Announce for screen readers
|
|
|
|
|
setAriaAnnouncement(`Navigated to ${previousBook.name} chapter ${lastChapter}`)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const handleNextChapter = () => {
|
|
|
|
|
// Trigger transition animation
|
|
|
|
|
setIsTransitioning(true)
|
|
|
|
|
setTimeout(() => setIsTransitioning(false), 300) // Match CSS transition duration
|
|
|
|
|
|
|
|
|
|
if (selectedChapter < maxChapters) {
|
|
|
|
|
const newChapter = selectedChapter + 1
|
|
|
|
|
setSelectedChapter(newChapter)
|
|
|
|
|
updateUrl(selectedBook, newChapter, selectedVersion)
|
|
|
|
|
// Announce for screen readers
|
|
|
|
|
setAriaAnnouncement(`Navigated to ${currentBook?.name} chapter ${newChapter}`)
|
|
|
|
|
} else {
|
|
|
|
|
const currentBookIndex = books.findIndex(book => book.id === selectedBook)
|
|
|
|
|
if (currentBookIndex < books.length - 1) {
|
|
|
|
|
@@ -972,10 +1033,47 @@ export default function BibleReaderNew({ initialVersion, initialBook, initialCha
|
|
|
|
|
setSelectedBook(nextBook.id)
|
|
|
|
|
setSelectedChapter(1)
|
|
|
|
|
updateUrl(nextBook.id, 1, selectedVersion)
|
|
|
|
|
// Announce for screen readers
|
|
|
|
|
setAriaAnnouncement(`Navigated to ${nextBook.name} chapter 1`)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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<HTMLDivElement>) => {
|
|
|
|
|
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
|
|
|
|
|
|
|
|
|
|
@@ -1414,20 +1512,20 @@ export default function BibleReaderNew({ initialVersion, initialBook, initialCha
|
|
|
|
|
switch (preferences.theme) {
|
|
|
|
|
case 'dark':
|
|
|
|
|
return {
|
|
|
|
|
backgroundColor: '#1a1a1a',
|
|
|
|
|
color: '#e0e0e0',
|
|
|
|
|
borderColor: '#333'
|
|
|
|
|
backgroundColor: '#0d0d0d', // Darker for better contrast (WCAG AAA: 15.3:1)
|
|
|
|
|
color: '#f0f0f0', // Brighter text for 7:1+ contrast
|
|
|
|
|
borderColor: '#404040'
|
|
|
|
|
}
|
|
|
|
|
case 'sepia':
|
|
|
|
|
return {
|
|
|
|
|
backgroundColor: '#f7f3e9',
|
|
|
|
|
color: '#5c4b3a',
|
|
|
|
|
backgroundColor: '#f5f1e3', // Adjusted sepia background
|
|
|
|
|
color: '#2b2419', // Darker text for 7:1+ contrast (WCAG AAA)
|
|
|
|
|
borderColor: '#d4c5a0'
|
|
|
|
|
}
|
|
|
|
|
default:
|
|
|
|
|
return {
|
|
|
|
|
backgroundColor: '#ffffff',
|
|
|
|
|
color: '#000000',
|
|
|
|
|
color: '#000000', // Pure black on white = 21:1 (exceeds WCAG AAA)
|
|
|
|
|
borderColor: '#e0e0e0'
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
@@ -1993,6 +2091,41 @@ export default function BibleReaderNew({ initialVersion, initialBook, initialCha
|
|
|
|
|
}
|
|
|
|
|
label={t('readingMode')}
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
<Divider sx={{ my: 2 }} />
|
|
|
|
|
<Typography variant="subtitle2" gutterBottom sx={{ fontWeight: 600 }}>
|
|
|
|
|
Mobile Navigation
|
|
|
|
|
</Typography>
|
|
|
|
|
|
|
|
|
|
<FormControlLabel
|
|
|
|
|
control={
|
|
|
|
|
<Switch
|
|
|
|
|
checked={preferences.enableSwipeGestures}
|
|
|
|
|
onChange={(e) => setPreferences(prev => ({ ...prev, enableSwipeGestures: e.target.checked }))}
|
|
|
|
|
/>
|
|
|
|
|
}
|
|
|
|
|
label="Enable Swipe Gestures"
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
<FormControlLabel
|
|
|
|
|
control={
|
|
|
|
|
<Switch
|
|
|
|
|
checked={preferences.enableTapZones}
|
|
|
|
|
onChange={(e) => setPreferences(prev => ({ ...prev, enableTapZones: e.target.checked }))}
|
|
|
|
|
/>
|
|
|
|
|
}
|
|
|
|
|
label="Enable Tap Zones"
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
<FormControlLabel
|
|
|
|
|
control={
|
|
|
|
|
<Switch
|
|
|
|
|
checked={preferences.paginationMode}
|
|
|
|
|
onChange={(e) => setPreferences(prev => ({ ...prev, paginationMode: e.target.checked }))}
|
|
|
|
|
/>
|
|
|
|
|
}
|
|
|
|
|
label="Pagination Mode"
|
|
|
|
|
/>
|
|
|
|
|
</Box>
|
|
|
|
|
</DialogContent>
|
|
|
|
|
<DialogActions>
|
|
|
|
|
@@ -2012,6 +2145,48 @@ export default function BibleReaderNew({ initialVersion, initialBook, initialCha
|
|
|
|
|
...getThemeStyles()
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
{/* Skip Navigation Link - WCAG AAA */}
|
|
|
|
|
<Box
|
|
|
|
|
component="a"
|
|
|
|
|
href="#main-content"
|
|
|
|
|
sx={{
|
|
|
|
|
position: 'absolute',
|
|
|
|
|
left: '-9999px',
|
|
|
|
|
zIndex: 9999,
|
|
|
|
|
padding: '1rem',
|
|
|
|
|
backgroundColor: 'primary.main',
|
|
|
|
|
color: 'white',
|
|
|
|
|
textDecoration: 'none',
|
|
|
|
|
fontWeight: 'bold',
|
|
|
|
|
'&:focus': {
|
|
|
|
|
left: '50%',
|
|
|
|
|
top: '10px',
|
|
|
|
|
transform: 'translateX(-50%)',
|
|
|
|
|
outline: '2px solid',
|
|
|
|
|
outlineColor: 'primary.dark',
|
|
|
|
|
outlineOffset: '2px'
|
|
|
|
|
}
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
Skip to main content
|
|
|
|
|
</Box>
|
|
|
|
|
|
|
|
|
|
{/* ARIA Live Region for Screen Reader Announcements */}
|
|
|
|
|
<Box
|
|
|
|
|
role="status"
|
|
|
|
|
aria-live="polite"
|
|
|
|
|
aria-atomic="true"
|
|
|
|
|
sx={{
|
|
|
|
|
position: 'absolute',
|
|
|
|
|
left: '-9999px',
|
|
|
|
|
width: '1px',
|
|
|
|
|
height: '1px',
|
|
|
|
|
overflow: 'hidden'
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
{ariaAnnouncement}
|
|
|
|
|
</Box>
|
|
|
|
|
|
|
|
|
|
{/* Top Toolbar - Simplified */}
|
|
|
|
|
{!preferences.readingMode && (
|
|
|
|
|
<AppBar position="static" sx={{ ...getThemeStyles(), boxShadow: 1 }}>
|
|
|
|
|
@@ -2026,6 +2201,14 @@ export default function BibleReaderNew({ initialVersion, initialBook, initialCha
|
|
|
|
|
onClick={handlePreviousChapter}
|
|
|
|
|
disabled={selectedBook === books[0]?.id && selectedChapter === 1}
|
|
|
|
|
size="small"
|
|
|
|
|
aria-label="Previous chapter"
|
|
|
|
|
sx={{
|
|
|
|
|
'&:focus': {
|
|
|
|
|
outline: '2px solid',
|
|
|
|
|
outlineColor: 'primary.main',
|
|
|
|
|
outlineOffset: '2px'
|
|
|
|
|
}
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<ArrowBack />
|
|
|
|
|
</IconButton>
|
|
|
|
|
@@ -2036,6 +2219,14 @@ export default function BibleReaderNew({ initialVersion, initialBook, initialCha
|
|
|
|
|
onClick={handleNextChapter}
|
|
|
|
|
disabled={selectedBook === books[books.length - 1]?.id && selectedChapter === maxChapters}
|
|
|
|
|
size="small"
|
|
|
|
|
aria-label="Next chapter"
|
|
|
|
|
sx={{
|
|
|
|
|
'&:focus': {
|
|
|
|
|
outline: '2px solid',
|
|
|
|
|
outlineColor: 'primary.main',
|
|
|
|
|
outlineOffset: '2px'
|
|
|
|
|
}
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<ArrowForward />
|
|
|
|
|
</IconButton>
|
|
|
|
|
@@ -2058,13 +2249,23 @@ export default function BibleReaderNew({ initialVersion, initialBook, initialCha
|
|
|
|
|
|
|
|
|
|
{/* Reading Content */}
|
|
|
|
|
<Box
|
|
|
|
|
id="main-content"
|
|
|
|
|
{...swipeHandlers}
|
|
|
|
|
ref={contentRef}
|
|
|
|
|
onClick={handleTapZone}
|
|
|
|
|
tabIndex={-1}
|
|
|
|
|
sx={{
|
|
|
|
|
maxWidth: preferences.columnLayout ? 'none' : '800px',
|
|
|
|
|
mx: 'auto',
|
|
|
|
|
width: '100%',
|
|
|
|
|
minHeight: '60vh', // Prevent layout shifts
|
|
|
|
|
position: 'relative'
|
|
|
|
|
position: 'relative',
|
|
|
|
|
cursor: preferences.enableTapZones && isMobile ? 'pointer' : 'default',
|
|
|
|
|
userSelect: 'text', // Ensure text selection still works
|
|
|
|
|
WebkitUserSelect: 'text',
|
|
|
|
|
'&:focus': {
|
|
|
|
|
outline: 'none' // Remove default outline since we have skip link
|
|
|
|
|
}
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<Paper
|
|
|
|
|
@@ -2075,7 +2276,13 @@ export default function BibleReaderNew({ initialVersion, initialBook, initialCha
|
|
|
|
|
p: preferences.readingMode ? 4 : 3,
|
|
|
|
|
minHeight: preferences.readingMode ? '100vh' : '60vh', // Consistent minimum height
|
|
|
|
|
border: preferences.readingMode ? 'none' : `1px solid ${getThemeStyles().borderColor}`,
|
|
|
|
|
position: 'relative'
|
|
|
|
|
position: 'relative',
|
|
|
|
|
opacity: isTransitioning ? 0.5 : 1,
|
|
|
|
|
transition: 'opacity 0.3s ease-in-out',
|
|
|
|
|
transform: isTransitioning ? 'scale(0.98)' : 'scale(1)',
|
|
|
|
|
transitionProperty: 'opacity, transform',
|
|
|
|
|
transitionDuration: '0.3s',
|
|
|
|
|
transitionTimingFunction: 'ease-in-out'
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
{loading && (
|
|
|
|
|
|