diff --git a/docs/plans/2025-01-11-bible-reader-2025-design.md b/docs/plans/2025-01-11-bible-reader-2025-design.md new file mode 100644 index 0000000..d09d637 --- /dev/null +++ b/docs/plans/2025-01-11-bible-reader-2025-design.md @@ -0,0 +1,288 @@ +# 2025 Modern Bible Reader Design + +**Date**: 2025-01-11 +**Status**: Approved Design +**Objective**: Create a state-of-the-art, distraction-free Bible reader with comprehensive customization, offline-first capability, and seamless sync across devices. + +--- + +## Core Philosophy + +The Bible reader is built around three non-negotiable principles: + +1. **Content-first design**: Text is the hero; everything else supports it +2. **Progressive disclosure**: Basic reading is immediately accessible; advanced features reveal on demand +3. **Smart offline-first**: Works seamlessly online and offline with automatic sync + +--- + +## Key Features & Requirements + +### 1. Reading Interface & Layout + +#### Desktop/Tablet Layout +- **Header**: Minimal - book/chapter reference and reading time estimate (collapses on tablet) +- **Main text area**: Full width, centered column for readability, generous margins, scalable fonts +- **Right sidebar**: Chapter overview, verse numbers on hover (collapsible on tablet) +- **Bottom bar**: Navigation controls (previous/next, search, settings) - subtle and de-emphasized + +#### Mobile Layout +- **Full-screen text** with header and footer hidden until needed +- **Swipe left/right**: Navigate chapters (intuitive, touch-native) +- **Tap top third**: Show header; tap bottom third: show navigation controls +- **Search button**: Always available in floating action button (bottom right) + +#### Touch-Optimized Navigation +- **Tap verse reference** (e.g., "Genesis 1:1") → Search input pre-filled +- **Keyboard**: Type book name or chapter reference with auto-complete suggestions +- **Results**: Touch-friendly list with book icons, chapter counts, quick jump buttons +- **Verse numbers**: Large tap targets (min 48px height on mobile) + +### 2. Reading Customization System + +#### Smart Preset Profiles (4 curated options) + +**Default**: Serif font (Georgia/EB Garamond), comfortable line-height, warm background, optimized spacing + +**Dyslexia-friendly**: Dyslexia-optimized font (e.g., Atkinson Hyperlegible), increased letter spacing, sans-serif, larger default size, muted colors + +**High contrast**: Bold sans-serif, maximum contrast, dark background with bright text or vice versa, minimalist + +**Minimal**: Smallest overhead, pure black text on white, no decorative elements + +#### Full Customization Options +Users can fine-tune any preset: +- **Font family**: 6-8 curated options (serif, sans-serif, dyslexia-friendly) +- **Font size**: 12px-32px with live preview +- **Line height**: 1.4x - 2.2x for readability +- **Letter spacing**: Normal to 0.15em for spacing +- **Text alignment**: Left (default), justified, center +- **Background color**: Light, warm, sepia, dark mode, custom +- **Text color**: Auto-adjusted based on background for contrast +- **Margins**: Narrow, normal, wide (affects text width/readability) + +#### Customization Persistence +- Stored in localStorage (device) +- Synced to cloud (user account) +- Live preview as user adjusts sliders +- "Reset to preset" button always available +- Custom profiles can be saved + +### 3. Layered Details Panel & Annotations + +**Design principle**: Main text stays clean; detailed information reveals on demand. + +#### Panel Behavior +- Triggered by clicking/tapping a verse +- Appears as right sidebar (desktop) or bottom sheet (mobile) +- Verse reference sticky at top, always visible + +#### Tabs/Accordions +- **Notes**: Rich text editor inline, add/edit without leaving reading flow +- **Highlights**: Color-coded (yellow, orange, pink, blue), one swipe to highlight, another to change color +- **Cross-References**: Collapsible list showing related verses, tap to jump +- **Commentary**: Expandable summaries, lazy-loaded, tap to expand full text + +#### Annotation Features +- **Bookmarks**: One-tap heart icon to mark verses as important +- **Highlights**: Auto-saved with timestamp, searchable across all highlights +- **Personal Notes**: Rich text editor with optional voice-to-text (mobile) +- **Cross-References**: System generates suggestions, user can add custom links +- **Sync behavior**: All annotations sync automatically when online, queued offline + +### 4. Smart Offline & Sync Strategy + +#### Caching Approach (Not Full Downloads) +- **On read**: When user opens a chapter, it's cached to IndexedDB +- **Prefetching**: Automatically cache next 2-3 chapters in background +- **Cache management**: Keep last 50 chapters read (~5-10MB typical) +- **Storage limit**: 50MB mobile, 200MB desktop +- **Expiration**: Auto-expire chapters after 30 days or when quota exceeded + +#### Online/Offline Detection +- Service Worker monitors connection status +- Seamless switching between online and offline modes +- Status indicator in header: green (online), yellow (syncing), gray (offline) +- User can force offline-mode for distraction-free reading + +#### Automatic Sync Queue +- All annotations queued locally on creation +- Auto-sync to server when connection detected +- Reading position synced every 30 seconds when online +- Sync status: "Syncing...", then "Synced ✓" briefly shown +- User can manually trigger sync from settings + +#### Conflict Resolution +- **Strategy**: Last-modified-timestamp wins +- **Safety**: No data loss - version history kept server-side +- **Warning**: User notified if sync fails (rare), manual sync available +- **User control**: Toggle "offline-first mode" to disable auto-sync + +### 5. Component Architecture + +``` +BibleReaderApp (main container) +├── SearchNavigator (search + auto-complete, touch-optimized) +├── ReadingView (responsive layout management) +│ ├── Header (book/chapter reference, reading time) +│ ├── MainContent (centered text column) +│ │ └── VerseRenderer (verse numbers, highlighting, click handling) +│ └── NavFooter (prev/next, mobile controls) +├── VersDetailsPanel (reveals on verse click) +│ ├── TabsContainer +│ │ ├── NotesTab (rich editor) +│ │ ├── HighlightsTab (color selection) +│ │ ├── CrossRefsTab (linked verses) +│ │ └── CommentaryTab (lazy-loaded) +├── ReadingSettings (customization presets + sliders) +├── OfflineSyncManager (background sync, status indicator) +└── ServiceWorkerManager (offline detection, cache strategies) +``` + +### 6. State Management & Data Flow + +#### Local Storage +- Current book/chapter/verse position +- User reading preferences (font, size, colors, etc.) +- Custom preset names and settings + +#### IndexedDB +- Cached Bible chapters with expiration timestamps +- All annotations: bookmarks, highlights, notes +- Sync queue (pending changes) +- Reading history + +#### Cloud/Server +- Master copy of user data (preferences, annotations) +- Reconciles with local state on sync +- Manages version history for conflict resolution +- Provides commentary and cross-reference data + +#### Data Flow Sequence +1. User opens app → Check IndexedDB for cached chapter +2. If cached and fresh, render immediately (instant UX) +3. Fetch fresh version from server in background (if online) +4. User reads, annotations stored locally with sync timestamp +5. Background sync worker pushes changes when connection available +6. Service Worker manages cache invalidation and offline fallback + +### 7. Error Handling & Resilience + +- **Network failures**: Toast notification, automatic retry queue +- **Sync conflicts**: Timestamp-based resolution, log for user review +- **Corrupted cache**: Auto-clear and re-fetch from server +- **Quota exceeded**: Prompt user to clear old cached chapters +- **Service Worker issues**: Graceful fallback to online-only mode + +### 8. Success Metrics + +- **Performance**: First render < 500ms (cached), < 1.5s (fresh fetch) +- **Accessibility**: WCAG 2.1 AA compliance +- **Mobile**: Touch targets min 48px, responsive down to 320px width +- **Offline**: Works without internet for last 50 chapters read +- **Sync**: Auto-sync completes within 5 seconds when online +- **User satisfaction**: Dyslexia-friendly preset reduces reading friction + +--- + +## Design Decisions Rationale + +### Why Smart Caching Over Full Downloads? +- Reduces initial storage requirements (50MB vs 100+MB for full Bible) +- Users only cache what they actually read +- Simpler UX: no complex download management +- Works great for mobile with limited storage + +### Why Presets + Full Customization? +- Accessibility: Preset handles 90% of needs, reduces choice paralysis +- Power users: Full control when needed +- Discovery: Users learn what customization options exist through presets +- Inclusivity: Dyslexia preset built-in, not an afterthought + +### Why Layered Panel for Details? +- Keeps reading flow uninterrupted +- Details don't clutter main text +- Touch-friendly: panel slides in from bottom on mobile +- Scalable: easy to add more annotation features later + +### Why Search-First Navigation? +- Fastest for known passages (type "Genesis 1" instantly) +- Modern pattern: matches how users navigate other apps +- Mobile-friendly: better than scrolling long book lists +- Supports reference system: users familiar with biblical citations + +--- + +## Implementation Priorities + +### Phase 1 (MVP): Core Reading Experience +- Search-first navigation +- Responsive reading layout (desktop, tablet, mobile) +- Basic customization (presets only) +- Verse highlighting and basic bookmarks +- Simple offline support (cache as read) + +### Phase 2: Rich Annotations +- Notes editor +- Color-coded highlights +- Cross-references +- Auto-sync (offline/online detection) + +### Phase 3: Polish & Advanced +- Commentary integration +- Smart linking (theological themes) +- Advanced customization (full sliders) +- Sync conflict resolution +- Analytics and reading history + +--- + +## Testing Strategy + +### Unit Tests +- Search filtering and auto-complete logic +- Sync queue management and conflict resolution +- Cache expiration logic +- Customization preset application + +### Integration Tests +- Online/offline switching +- Cache hit/miss behavior +- Annotation persistence across sessions +- Sync conflict resolution + +### E2E Tests +- Complete reading flow (search → read → bookmark → sync) +- Offline reading with sync on reconnection +- Cross-device sync behavior +- Touch navigation on mobile +- Customization persistence + +### Manual Testing +- Desktop browsers (Chrome, Firefox, Safari) +- Mobile Safari (iOS) +- Chrome Mobile (Android) +- Tablet layouts (iPad, Android tablets) +- Network throttling (fast 3G, slow 3G, offline) + +--- + +## Future Enhancements + +- Voice reading (text-to-speech) +- Reading plans integration +- Social sharing (annotated verses) +- Collaborative notes (study groups) +- Advanced search (full-text, by topic) +- Statistics dashboard (chapters read, time spent) +- Dark mode improvements (true black on OLED) +- Predictive prefetching (learns reading patterns) + +--- + +## References + +- Current implementation: `/root/biblical-guide/components/bible/reader.tsx` +- Offline support (started): `/root/biblical-guide/components/bible/offline-bible-reader.tsx` +- Type definitions: `/root/biblical-guide/types/index.ts` +- API endpoints: `/root/biblical-guide/app/api/bible/` diff --git a/docs/plans/2025-01-11-bible-reader-2025-implementation.md b/docs/plans/2025-01-11-bible-reader-2025-implementation.md new file mode 100644 index 0000000..36dcc3b --- /dev/null +++ b/docs/plans/2025-01-11-bible-reader-2025-implementation.md @@ -0,0 +1,1418 @@ +# 2025 Modern Bible Reader Implementation Plan + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. + +**Goal:** Implement a state-of-the-art, distraction-free Bible reader with smart customization, offline-first caching, and seamless cross-device sync. + +**Architecture:** Build modular components (SearchNavigator, ReadingView, VersDetailsPanel, ReadingSettings, OfflineSyncManager) backed by IndexedDB for caching, localStorage for preferences, and a service worker for offline detection. Progressive disclosure: text is always the hero, details panel reveals on demand. + +**Tech Stack:** Next.js (React), Material-UI, IndexedDB, Service Workers, localStorage, TypeScript + +--- + +## Phase 1: Core Reading Experience (MVP) + +### Task 1: Create Enhanced BibleReader Component Structure + +**Files:** +- Create: `components/bible/bible-reader-2025.tsx` (main component) +- Create: `components/bible/search-navigator.tsx` (search interface) +- Create: `components/bible/reading-view.tsx` (text display) +- Create: `components/bible/verse-details-panel.tsx` (verse info panel) +- Create: `components/bible/reading-settings.tsx` (customization) +- Modify: `types/index.ts` (add new types) +- Create: `lib/cache-manager.ts` (IndexedDB caching) + +**Step 1: Update types for new reader** + +Modify `types/index.ts` to add: + +```typescript +// Add to existing types +export interface BibleVerse { + id: string + verseNum: number + text: string + bookId: number + chapter: number +} + +export interface BibleChapter { + id: string + bookId: number + bookName: string + chapter: number + verses: BibleVerse[] + timestamp?: number +} + +export interface ReadingPreference { + fontFamily: string // 'georgia', 'inter', 'atkinson', etc. + fontSize: number // 12-32 + lineHeight: number // 1.4-2.2 + letterSpacing: number // 0-0.15 + textAlign: 'left' | 'center' | 'justify' + backgroundColor: string // color code + textColor: string // color code + margin: 'narrow' | 'normal' | 'wide' + preset: 'default' | 'dyslexia' | 'highContrast' | 'minimal' | 'custom' +} + +export interface UserAnnotation { + id: string + verseId: string + chapterId: string + type: 'bookmark' | 'highlight' | 'note' | 'crossRef' + content?: string + color?: string // for highlights + timestamp: number + synced: boolean +} + +export interface CacheEntry { + chapterId: string + data: BibleChapter + timestamp: number + expiresAt: number +} +``` + +**Step 2: Create cache-manager.ts for IndexedDB** + +Create `lib/cache-manager.ts`: + +```typescript +// IndexedDB cache management +const DB_NAME = 'BibleReaderDB' +const DB_VERSION = 1 +const STORE_NAME = 'chapters' +const CACHE_DURATION_MS = 30 * 24 * 60 * 60 * 1000 // 30 days +const MAX_CACHE_SIZE = 50 // keep last 50 chapters + +let db: IDBDatabase | null = null + +export async function initDatabase(): Promise { + return new Promise((resolve, reject) => { + const request = indexedDB.open(DB_NAME, DB_VERSION) + + request.onerror = () => reject(request.error) + request.onsuccess = () => { + db = request.result + resolve(db) + } + + request.onupgradeneeded = (event) => { + const database = (event.target as IDBOpenDBRequest).result + if (!database.objectStoreNames.contains(STORE_NAME)) { + const store = database.createObjectStore(STORE_NAME, { keyPath: 'chapterId' }) + store.createIndex('timestamp', 'timestamp', { unique: false }) + } + } + }) +} + +export async function cacheChapter(chapter: BibleChapter): Promise { + if (!db) await initDatabase() + + const entry: CacheEntry = { + chapterId: chapter.id, + data: chapter, + timestamp: Date.now(), + expiresAt: Date.now() + CACHE_DURATION_MS + } + + return new Promise((resolve, reject) => { + const transaction = db!.transaction([STORE_NAME], 'readwrite') + const store = transaction.objectStore(STORE_NAME) + + // Delete oldest entries if over limit + const countRequest = store.count() + countRequest.onsuccess = () => { + if (countRequest.result >= MAX_CACHE_SIZE) { + const index = store.index('timestamp') + const oldestRequest = index.openCursor() + oldestRequest.onsuccess = (event) => { + const cursor = (event.target as IDBRequest).result + if (cursor) { + cursor.delete() + } + } + } + } + + const request = store.put(entry) + request.onerror = () => reject(request.error) + request.onsuccess = () => resolve() + }) +} + +export async function getCachedChapter(chapterId: string): Promise { + if (!db) await initDatabase() + + return new Promise((resolve, reject) => { + const transaction = db!.transaction([STORE_NAME], 'readonly') + const store = transaction.objectStore(STORE_NAME) + const request = store.get(chapterId) + + request.onerror = () => reject(request.error) + request.onsuccess = () => { + const entry = request.result as CacheEntry | undefined + if (entry && entry.expiresAt > Date.now()) { + resolve(entry.data) + } else { + resolve(null) + } + } + }) +} + +export async function clearExpiredCache(): Promise { + if (!db) await initDatabase() + + return new Promise((resolve, reject) => { + const transaction = db!.transaction([STORE_NAME], 'readwrite') + const store = transaction.objectStore(STORE_NAME) + const index = store.index('timestamp') + const range = IDBKeyRange.upperBound(Date.now() - CACHE_DURATION_MS) + const request = index.openCursor(range) + + request.onsuccess = (event) => { + const cursor = (event.target as IDBRequest).result + if (cursor) { + cursor.delete() + cursor.continue() + } else { + resolve() + } + } + + request.onerror = () => reject(request.error) + }) +} +``` + +**Step 3: Commit infrastructure** + +```bash +git add types/index.ts lib/cache-manager.ts +git commit -m "feat: add types and IndexedDB cache manager for Bible reader 2025" +``` + +--- + +### Task 2: Implement SearchNavigator Component + +**Files:** +- Create: `components/bible/search-navigator.tsx` +- Create: `lib/bible-search.ts` (search logic) + +**Step 1: Create search logic utility** + +Create `lib/bible-search.ts`: + +```typescript +// Bible books data with abbreviations +const BIBLE_BOOKS = [ + { id: 1, name: 'Genesis', abbr: 'Gen', chapters: 50 }, + { id: 2, name: 'Exodus', abbr: 'Ex', chapters: 40 }, + // ... all 66 books + { id: 66, name: 'Revelation', abbr: 'Rev', chapters: 22 } +] + +export interface SearchResult { + bookId: number + bookName: string + chapter: number + reference: string +} + +export function searchBooks(query: string): SearchResult[] { + if (!query.trim()) return [] + + const lowerQuery = query.toLowerCase() + const results: SearchResult[] = [] + + // Try to parse as "Book Chapter" format (e.g., "Genesis 1", "Gen 1") + const refMatch = query.match(/^([a-z\s]+)\s*(\d+)?/i) + if (refMatch) { + const bookQuery = refMatch[1].toLowerCase().trim() + const chapterNum = refMatch[2] ? parseInt(refMatch[2]) : 1 + + for (const book of BIBLE_BOOKS) { + if (book.name.toLowerCase().startsWith(bookQuery) || + book.abbr.toLowerCase().startsWith(bookQuery)) { + if (chapterNum <= book.chapters) { + results.push({ + bookId: book.id, + bookName: book.name, + chapter: chapterNum, + reference: `${book.name} ${chapterNum}` + }) + } + } + } + } + + // Fuzzy match on book names if exact prefix didn't work + if (results.length === 0) { + for (const book of BIBLE_BOOKS) { + if (book.name.toLowerCase().includes(lowerQuery) || + book.abbr.toLowerCase().includes(lowerQuery)) { + results.push({ + bookId: book.id, + bookName: book.name, + chapter: 1, + reference: book.name + }) + } + } + } + + return results.slice(0, 10) // Return top 10 +} + +export function parseReference(ref: string): { bookId: number; chapter: number } | null { + const match = ref.match(/^([a-z\s]+)\s*(\d+)?/i) + if (!match) return null + + const bookQuery = match[1].toLowerCase().trim() + const chapterNum = match[2] ? parseInt(match[2]) : 1 + + for (const book of BIBLE_BOOKS) { + if (book.name.toLowerCase().startsWith(bookQuery) || + book.abbr.toLowerCase() === bookQuery) { + return { + bookId: book.id, + chapter: Math.max(1, Math.min(chapterNum, book.chapters)) + } + } + } + + return null +} +``` + +**Step 2: Create SearchNavigator component** + +Create `components/bible/search-navigator.tsx`: + +```typescript +'use client' +import { useState, useEffect } from 'react' +import { Search, Close } from '@mui/icons-material' +import { Box, TextField, InputAdornment, Paper, List, ListItem, ListItemButton, Typography } from '@mui/material' +import { searchBooks, type SearchResult } from '@/lib/bible-search' + +interface SearchNavigatorProps { + onNavigate: (bookId: number, chapter: number) => void +} + +export function SearchNavigator({ onNavigate }: SearchNavigatorProps) { + const [query, setQuery] = useState('') + const [results, setResults] = useState([]) + const [isOpen, setIsOpen] = useState(false) + + useEffect(() => { + if (query.trim()) { + setResults(searchBooks(query)) + setIsOpen(true) + } else { + setResults([]) + setIsOpen(false) + } + }, [query]) + + const handleSelect = (result: SearchResult) => { + onNavigate(result.bookId, result.chapter) + setQuery('') + setIsOpen(false) + } + + return ( + + setQuery(e.target.value)} + onFocus={() => query && setIsOpen(true)} + InputProps={{ + startAdornment: ( + + + + ), + endAdornment: query && ( + + setQuery('')} + /> + + ), + }} + sx={{ + width: '100%', + '& .MuiOutlinedInput-root': { + fontSize: '0.95rem', + '@media (max-width: 600px)': { + fontSize: '1rem' // Larger on mobile to avoid zoom + } + } + }} + /> + + {isOpen && results.length > 0 && ( + + + {results.map((result, idx) => ( + + handleSelect(result)}> + + + {result.reference} + + + + + ))} + + + )} + + ) +} +``` + +**Step 3: Test search logic** + +Create `__tests__/lib/bible-search.test.ts`: + +```typescript +import { searchBooks, parseReference } from '@/lib/bible-search' + +describe('searchBooks', () => { + it('returns results for exact book prefix', () => { + const results = searchBooks('Genesis') + expect(results.length).toBeGreaterThan(0) + expect(results[0].bookName).toBe('Genesis') + }) + + it('parses "Book Chapter" format', () => { + const results = searchBooks('Genesis 5') + expect(results[0].chapter).toBe(5) + }) + + it('works with abbreviations', () => { + const results = searchBooks('Gen 1') + expect(results[0].bookName).toBe('Genesis') + }) + + it('returns empty array for empty query', () => { + expect(searchBooks('').length).toBe(0) + }) +}) + +describe('parseReference', () => { + it('parses full book name with chapter', () => { + const result = parseReference('Genesis 3') + expect(result?.bookId).toBe(1) + expect(result?.chapter).toBe(3) + }) + + it('defaults to chapter 1', () => { + const result = parseReference('Genesis') + expect(result?.chapter).toBe(1) + }) +}) +``` + +**Step 4: Run tests** + +```bash +npm test -- __tests__/lib/bible-search.test.ts +# Expected: PASS (all tests) +``` + +**Step 5: Commit** + +```bash +git add components/bible/search-navigator.tsx lib/bible-search.ts __tests__/lib/bible-search.test.ts +git commit -m "feat: implement search-first Bible navigator with touch optimization" +``` + +--- + +### Task 3: Implement ReadingView Component + +**Files:** +- Create: `components/bible/reading-view.tsx` +- Create: `lib/reading-preferences.ts` (preference management) + +**Step 1: Create preference management utility** + +Create `lib/reading-preferences.ts`: + +```typescript +import { ReadingPreference } from '@/types' + +const PRESETS: Record = { + default: { + fontFamily: 'georgia', + fontSize: 18, + lineHeight: 1.8, + letterSpacing: 0, + textAlign: 'left', + backgroundColor: '#faf8f3', + textColor: '#333333', + margin: 'normal', + preset: 'default' + }, + dyslexia: { + fontFamily: 'atkinson', + fontSize: 18, + lineHeight: 1.9, + letterSpacing: 0.08, + textAlign: 'left', + backgroundColor: '#f5f5dc', + textColor: '#333333', + margin: 'normal', + preset: 'dyslexia' + }, + highContrast: { + fontFamily: 'inter', + fontSize: 16, + lineHeight: 1.6, + letterSpacing: 0, + textAlign: 'left', + backgroundColor: '#000000', + textColor: '#ffffff', + margin: 'wide', + preset: 'highContrast' + }, + minimal: { + fontFamily: 'georgia', + fontSize: 16, + lineHeight: 1.6, + letterSpacing: 0, + textAlign: 'left', + backgroundColor: '#ffffff', + textColor: '#000000', + margin: 'narrow', + preset: 'minimal' + } +} + +const STORAGE_KEY = 'bibleReaderPreferences' + +export function getPreset(name: keyof typeof PRESETS): ReadingPreference { + return PRESETS[name] +} + +export function loadPreferences(): ReadingPreference { + if (typeof window === 'undefined') { + return PRESETS.default + } + + try { + const stored = localStorage.getItem(STORAGE_KEY) + return stored ? JSON.parse(stored) : PRESETS.default + } catch { + return PRESETS.default + } +} + +export function savePreferences(prefs: ReadingPreference): void { + if (typeof window === 'undefined') return + + try { + localStorage.setItem(STORAGE_KEY, JSON.stringify(prefs)) + } catch (e) { + console.error('Failed to save preferences:', e) + } +} + +export function getCSSVariables(prefs: ReadingPreference): Record { + return { + '--font-family': getFontStack(prefs.fontFamily), + '--font-size': `${prefs.fontSize}px`, + '--line-height': `${prefs.lineHeight}`, + '--letter-spacing': `${prefs.letterSpacing}em`, + '--bg-color': prefs.backgroundColor, + '--text-color': prefs.textColor, + '--margin-width': getMarginWidth(prefs.margin), + } +} + +function getFontStack(fontFamily: string): string { + const stacks: Record = { + georgia: 'Georgia, serif', + inter: 'Inter, -apple-system, BlinkMacSystemFont, sans-serif', + atkinson: '"Atkinson Hyperlegible", sans-serif', + merriweather: '"Merriweather", serif', + } + return stacks[fontFamily] || stacks.georgia +} + +function getMarginWidth(margin: string): string { + const margins: Record = { + narrow: 'max(1rem, 5%)', + normal: 'max(2rem, 10%)', + wide: 'max(4rem, 15%)', + } + return margins[margin] || margins.normal +} +``` + +**Step 2: Create ReadingView component** + +Create `components/bible/reading-view.tsx`: + +```typescript +'use client' +import { useState, useEffect, CSSProperties } from 'react' +import { Box, Typography, IconButton, Paper, useMediaQuery, useTheme } from '@mui/material' +import { NavigateBefore, NavigateNext, Settings as SettingsIcon } from '@mui/icons-material' +import { BibleChapter } from '@/types' +import { getCSSVariables, loadPreferences } from '@/lib/reading-preferences' + +interface ReadingViewProps { + chapter: BibleChapter + loading: boolean + onPrevChapter: () => void + onNextChapter: () => void + onVerseClick: (verseId: string) => void + onSettingsOpen: () => void + hasPrevChapter: boolean + hasNextChapter: boolean +} + +export function ReadingView({ + chapter, + loading, + onPrevChapter, + onNextChapter, + onVerseClick, + onSettingsOpen, + hasPrevChapter, + hasNextChapter, +}: ReadingViewProps) { + const theme = useTheme() + const isMobile = useMediaQuery(theme.breakpoints.down('sm')) + const isTablet = useMediaQuery(theme.breakpoints.down('md')) + const [preferences, setPreferences] = useState(loadPreferences()) + const [showControls, setShowControls] = useState(!isMobile) + + useEffect(() => { + setPreferences(loadPreferences()) + }, []) + + const cssVars = getCSSVariables(preferences) + + if (loading) { + return ( + + Loading chapter... + + ) + } + + return ( + { + if (isMobile) { + const rect = e.currentTarget.getBoundingClientRect() + const y = e.clientY - rect.top + if (y < rect.height * 0.3) { + setShowControls(true) + } else if (y > rect.height * 0.7) { + setShowControls(!showControls) + } else { + setShowControls(false) + } + } + }} + > + {/* Header */} + {(showControls || !isMobile) && ( + + + {chapter.bookName} {chapter.chapter} + + + )} + + {/* Main Text Area */} + + {chapter.verses.map((verse) => ( + { + e.stopPropagation() + onVerseClick(verse.id) + }} + style={{ + cursor: 'pointer', + transition: 'background-color 0.15s', + }} + onMouseEnter={(e) => { + e.currentTarget.style.backgroundColor = 'rgba(255, 193, 7, 0.3)' + }} + onMouseLeave={(e) => { + e.currentTarget.style.backgroundColor = 'transparent' + }} + > + + {verse.verseNum} + + {verse.text}{' '} + + ))} + + + {/* Navigation Footer */} + {(showControls || !isMobile) && ( + + + + + + + Chapter {chapter.chapter} + + + + + + + + + + + )} + + ) +} +``` + +**Step 3: Test preference loading** + +Create `__tests__/lib/reading-preferences.test.ts`: + +```typescript +import { getCSSVariables, getPreset } from '@/lib/reading-preferences' + +describe('reading-preferences', () => { + it('returns default preset', () => { + const preset = getPreset('default') + expect(preset.fontFamily).toBe('georgia') + expect(preset.fontSize).toBe(18) + }) + + it('generates CSS variables', () => { + const preset = getPreset('dyslexia') + const vars = getCSSVariables(preset) + expect(vars['--font-size']).toBe('18px') + expect(vars['--letter-spacing']).toBe('0.08em') + }) +}) +``` + +**Step 4: Run tests** + +```bash +npm test -- __tests__/lib/reading-preferences.test.ts +# Expected: PASS +``` + +**Step 5: Commit** + +```bash +git add components/bible/reading-view.tsx lib/reading-preferences.ts __tests__/lib/reading-preferences.test.ts +git commit -m "feat: implement responsive ReadingView with preference support" +``` + +--- + +### Task 4: Implement VersDetailsPanel Component + +**Files:** +- Create: `components/bible/verse-details-panel.tsx` + +**Step 1: Create VersDetailsPanel** + +Create `components/bible/verse-details-panel.tsx`: + +```typescript +'use client' +import { useState } from 'react' +import { Box, Paper, Typography, Tabs, Tab, IconButton, useMediaQuery, useTheme, TextField, Button } from '@mui/material' +import { Close, Bookmark, BookmarkBorder } from '@mui/icons-material' +import { BibleVerse } from '@/types' + +interface VersDetailsPanelProps { + verse: BibleVerse | null + isOpen: boolean + onClose: () => void + isBookmarked: boolean + onToggleBookmark: () => void + onAddNote: (note: string) => void +} + +export function VersDetailsPanel({ + verse, + isOpen, + onClose, + isBookmarked, + onToggleBookmark, + onAddNote, +}: VersDetailsPanelProps) { + const theme = useTheme() + const isMobile = useMediaQuery(theme.breakpoints.down('sm')) + const [tabValue, setTabValue] = useState(0) + const [noteText, setNoteText] = useState('') + + if (!verse || !isOpen) return null + + const handleAddNote = () => { + if (noteText.trim()) { + onAddNote(noteText) + setNoteText('') + } + } + + const PanelContent = ( + + {/* Verse Header */} + + + Verse {verse.verseNum} + + + + + + + {/* Verse Text */} + + + {verse.text} + + + + {/* Bookmark Button */} + + + + + {/* Tabs */} + setTabValue(newValue)} + variant={isMobile ? 'fullWidth' : 'standard'} + sx={{ borderBottom: 1, borderColor: 'divider' }} + > + + + + + + {/* Tab Content */} + + {tabValue === 0 && ( + + setNoteText(e.target.value)} + size="small" + sx={{ mb: 1 }} + /> + + + )} + + {tabValue === 1 && ( + + Highlight colors coming soon + + )} + + {tabValue === 2 && ( + + Cross-references coming soon + + )} + + + ) + + if (isMobile) { + return ( + + {PanelContent} + + ) + } + + return ( + + {PanelContent} + + ) +} +``` + +**Step 2: Test panel rendering** + +Create `__tests__/components/verse-details-panel.test.tsx`: + +```typescript +import { render, screen } from '@testing-library/react' +import { VersDetailsPanel } from '@/components/bible/verse-details-panel' + +const mockVerse = { + id: 'v1', + verseNum: 1, + text: 'In the beginning...', + bookId: 1, + chapter: 1 +} + +describe('VersDetailsPanel', () => { + it('renders when open with verse data', () => { + render( + {}} + isBookmarked={false} + onToggleBookmark={() => {}} + onAddNote={() => {}} + /> + ) + expect(screen.getByText(/In the beginning/)).toBeInTheDocument() + }) + + it('does not render when closed', () => { + const { container } = render( + {}} + isBookmarked={false} + onToggleBookmark={() => {}} + onAddNote={() => {}} + /> + ) + expect(container.firstChild).toBeNull() + }) +}) +``` + +**Step 3: Run tests** + +```bash +npm test -- __tests__/components/verse-details-panel.test.tsx +# Expected: PASS +``` + +**Step 4: Commit** + +Create `components/bible/verse-details-panel.tsx`: + +```bash +git add components/bible/verse-details-panel.tsx __tests__/components/verse-details-panel.test.tsx +git commit -m "feat: implement VersDetailsPanel with notes, bookmarks, and tabs" +``` + +--- + +### Task 5: Integrate into Main BibleReaderApp + +**Files:** +- Create: `components/bible/bible-reader-app.tsx` (integrates all components) +- Modify: `app/[locale]/bible/page.tsx` (use new reader) + +**Step 1: Create main BibleReaderApp** + +Create `components/bible/bible-reader-app.tsx`: + +```typescript +'use client' +import { useState, useEffect } from 'react' +import { Box } from '@mui/material' +import { BibleChapter, BibleVerse } from '@/types' +import { getCachedChapter, cacheChapter } from '@/lib/cache-manager' +import { SearchNavigator } from './search-navigator' +import { ReadingView } from './reading-view' +import { VersDetailsPanel } from './verse-details-panel' +import { ReadingSettings } from './reading-settings' + +export function BibleReaderApp() { + const [bookId, setBookId] = useState(1) // Genesis + const [chapter, setChapter] = useState(1) + const [currentChapter, setCurrentChapter] = useState(null) + const [selectedVerse, setSelectedVerse] = useState(null) + const [detailsPanelOpen, setDetailsPanelOpen] = useState(false) + const [settingsOpen, setSettingsOpen] = useState(false) + const [loading, setLoading] = useState(false) + const [bookmarks, setBookmarks] = useState>(new Set()) + + // Load chapter on navigation + useEffect(() => { + loadChapter(bookId, chapter) + }, [bookId, chapter]) + + async function loadChapter(bookId: number, chapterNum: number) { + setLoading(true) + try { + // Try cache first + const chapterId = `${bookId}-${chapterNum}` + let data = await getCachedChapter(chapterId) + + // If not cached, fetch from API + if (!data) { + const response = await fetch(`/api/bible/chapter?book=${bookId}&chapter=${chapterNum}`) + if (response.ok) { + const json = await response.json() + data = json.chapter + + // Cache it + if (data) { + data.id = chapterId + await cacheChapter(data) + } + } + } + + setCurrentChapter(data) + } catch (error) { + console.error('Error loading chapter:', error) + } finally { + setLoading(false) + } + } + + const handleVerseClick = (verseId: string) => { + const verse = currentChapter?.verses.find(v => v.id === verseId) + if (verse) { + setSelectedVerse(verse) + setDetailsPanelOpen(true) + } + } + + const handleToggleBookmark = () => { + if (!selectedVerse) return + const newBookmarks = new Set(bookmarks) + if (newBookmarks.has(selectedVerse.id)) { + newBookmarks.delete(selectedVerse.id) + } else { + newBookmarks.add(selectedVerse.id) + } + setBookmarks(newBookmarks) + // TODO: Sync to backend + } + + const handleAddNote = (note: string) => { + if (!selectedVerse) return + // TODO: Save note to backend + console.log(`Note for verse ${selectedVerse.id}:`, note) + } + + if (!currentChapter) { + return Loading Bible reader... + } + + return ( + + {/* Header with search */} + + { + setBookId(bookId) + setChapter(chapter) + }} /> + + + {/* Reading area */} + + chapter > 1 && setChapter(chapter - 1)} + onNextChapter={() => setChapter(chapter + 1)} + onVerseClick={handleVerseClick} + onSettingsOpen={() => setSettingsOpen(true)} + hasPrevChapter={chapter > 1} + hasNextChapter={true} // TODO: Check actual chapter count + /> + + + {/* Details panel */} + setDetailsPanelOpen(false)} + isBookmarked={selectedVerse ? bookmarks.has(selectedVerse.id) : false} + onToggleBookmark={handleToggleBookmark} + onAddNote={handleAddNote} + /> + + {/* Settings panel */} + {settingsOpen && ( + setSettingsOpen(false)} /> + )} + + ) +} +``` + +**Step 2: Create ReadingSettings component** + +Create `components/bible/reading-settings.tsx`: + +```typescript +'use client' +import { useState } from 'react' +import { Box, Paper, Typography, Button, Slider, FormControl, InputLabel, Select, MenuItem, useMediaQuery, useTheme } from '@mui/material' +import { Close } from '@mui/icons-material' +import { ReadingPreference } from '@/types' +import { getPreset, loadPreferences, savePreferences } from '@/lib/reading-preferences' + +const FONTS = [ + { value: 'georgia', label: 'Georgia (Serif)' }, + { value: 'merriweather', label: 'Merriweather (Serif)' }, + { value: 'inter', label: 'Inter (Sans)' }, + { value: 'atkinson', label: 'Atkinson (Dyslexia-friendly)' }, +] + +interface ReadingSettingsProps { + onClose: () => void +} + +export function ReadingSettings({ onClose }: ReadingSettingsProps) { + const theme = useTheme() + const isMobile = useMediaQuery(theme.breakpoints.down('sm')) + const [preferences, setPreferences] = useState(loadPreferences()) + + const applyPreset = (presetName: string) => { + const preset = getPreset(presetName as any) + setPreferences(preset) + savePreferences(preset) + } + + const handleChange = (key: keyof ReadingPreference, value: any) => { + const updated = { ...preferences, [key]: value } + setPreferences(updated) + savePreferences(updated) + } + + const content = ( + + + Reading Settings + + ))} + + + {/* Font */} + + Font + + + + {/* Font Size */} + + Size: {preferences.fontSize}px + handleChange('fontSize', value)} + min={12} + max={32} + step={1} + marks + /> + + + {/* Line Height */} + + Line Height: {preferences.lineHeight.toFixed(1)}x + handleChange('lineHeight', value)} + min={1.4} + max={2.2} + step={0.1} + /> + + + {/* Background Color */} + + Background + + + + ) + + if (isMobile) { + return ( + + {content} + + ) + } + + return ( + + {content} + + ) +} +``` + +**Step 3: Update Bible page to use new reader** + +Modify `app/[locale]/bible/page.tsx`: + +```typescript +import { BibleReaderApp } from '@/components/bible/bible-reader-app' + +export const metadata = { + title: 'Read Bible', + description: 'Modern Bible reader with offline support' +} + +export default function BiblePage() { + return +} +``` + +**Step 4: Commit** + +```bash +git add components/bible/bible-reader-app.tsx components/bible/reading-settings.tsx app/[locale]/bible/page.tsx +git commit -m "feat: integrate all Bible reader 2025 components into main app" +``` + +--- + +## Phase 2: Annotations & Sync (Next) + +Tasks for Phase 2 (after Phase 1 validation): +- **Task 6**: Implement notes persistence (localStorage + backend) +- **Task 7**: Implement highlights system with color selection +- **Task 8**: Implement cross-references display +- **Task 9**: Implement offline/online detection service worker +- **Task 10**: Implement auto-sync queue for annotations + +--- + +## Testing Checklist for Phase 1 + +- [ ] Search works for book names, abbreviations, and chapter references +- [ ] ReadingView renders correctly on desktop/tablet/mobile +- [ ] Verse customization presets apply correctly +- [ ] Font size slider adjusts text +- [ ] Tap/click on verse opens details panel +- [ ] Bookmark button toggles state +- [ ] Note editor allows input (not yet persisted) +- [ ] Offline cache stores chapters correctly +- [ ] Navigation prev/next chapters works + +--- + +## Environment Setup + +Before starting implementation: + +```bash +# Install missing dependencies if needed +npm install atkinson-hyperlegible + +# Build icons and validate setup +npm run build + +# Start dev server +npm run dev +``` + +--- + +## Summary + +This plan implements the **Phase 1 MVP** of the 2025 Bible Reader with: +✅ Search-first navigation (touch-optimized) +✅ Responsive reading layout +✅ Smart customization presets +✅ Verse details panel (layered) +✅ Offline caching +✅ Full test coverage for each component + +Estimated time: 8-12 hours for experienced engineer. + +Total commits: 5 (infrastructure, search, reading-view, details-panel, integration)