build: production build with Phase 1 2025 Bible Reader implementation complete

Includes all Phase 1 features:
- Search-first navigation with auto-complete
- Responsive reading interface (desktop/tablet/mobile)
- 4 customization presets + full fine-tuning controls
- Layered details panel with notes, bookmarks, highlights
- Smart offline caching with IndexedDB and auto-sync
- Full accessibility (WCAG 2.1 AA)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-11-11 20:38:01 +00:00
parent b8652b9f0a
commit 9b5c0ed8bb
50 changed files with 20146 additions and 859 deletions

948
PARALLEL_BIBLE_VIEW_PLAN.md Normal file
View File

@@ -0,0 +1,948 @@
# Parallel Bible View - Implementation Plan
## 📋 Overview
Implement a side-by-side Bible reading experience allowing users to compare multiple translations simultaneously, perfect for Bible study, translation verification, and deep Scripture analysis.
**Status:** Planning Phase
**Priority:** 🔴 High
**Estimated Time:** 2 weeks (80 hours)
**Target Completion:** TBD
---
## 🎯 Goals & Objectives
### Primary Goals
1. Enable simultaneous viewing of 2-3 Bible translations
2. Provide synchronized scrolling across all panes
3. Allow easy switching between versions
4. Maintain responsive design for mobile devices
5. Support independent highlighting per version
### User Value Proposition
- **For Bible students**: Compare translations to understand nuances
- **For scholars**: Analyze textual differences
- **For language learners**: See original and translated text
- **For teachers**: Prepare lessons with multiple versions
- **For translators**: Verify accuracy against source texts
---
## ✨ Feature Specifications
### 1. Layout Configurations
```typescript
type PaneLayout = '1-pane' | '2-pane-horizontal' | '2-pane-vertical' | '3-pane' | '4-pane'
interface LayoutConfig {
layout: PaneLayout
panes: PaneConfig[]
syncScroll: boolean
syncChapter: boolean // All panes show same chapter
equalWidths: boolean
showDividers: boolean
compactMode: boolean // Reduce padding on mobile
}
interface PaneConfig {
id: string
versionId: string
visible: boolean
width: number // percentage (for horizontal layouts)
locked: boolean // Prevent accidental changes
customSettings?: {
fontSize?: number
theme?: 'light' | 'dark' | 'sepia'
showVerseNumbers?: boolean
}
}
```
### 2. Visual Layouts
#### Desktop Layouts
```
2-Pane Horizontal:
┌─────────────────┬─────────────────┐
│ KJV │ ESV │
│ │ │
│ Genesis 1:1 │ Genesis 1:1 │
│ In the │ In the │
│ beginning... │ beginning... │
│ │ │
└─────────────────┴─────────────────┘
3-Pane:
┌───────┬───────┬───────┐
│ KJV │ ESV │ NIV │
│ │ │ │
│ Gen 1 │ Gen 1 │ Gen 1 │
│ │ │ │
└───────┴───────┴───────┘
2-Pane Vertical (Stacked):
┌─────────────────────────┐
│ KJV - Genesis 1 │
│ │
│ 1 In the beginning... │
└─────────────────────────┘
┌─────────────────────────┐
│ ESV - Genesis 1 │
│ │
│ 1 In the beginning... │
└─────────────────────────┘
```
#### Mobile Layout
```
Mobile (Stacked with Tabs):
┌─────────────────────────┐
│ [KJV] [ESV] [NIV] [+] │ ← Tab bar
├─────────────────────────┤
│ Genesis 1:1-31 │
│ │
│ 1 In the beginning... │
│ 2 And the earth... │
│ │
│ ▼ Swipe to compare ▼ │
└─────────────────────────┘
```
### 3. Synchronized Scrolling
```typescript
interface ScrollSyncConfig {
enabled: boolean
mode: 'verse' | 'pixel' | 'paragraph'
leadPane: string | 'any' // Which pane controls scroll
smoothness: number // 0-1, animation easing
threshold: number // Minimum scroll delta to trigger sync
}
class ScrollSynchronizer {
private panes: HTMLElement[]
private isScrolling: boolean = false
private scrollTimeout: NodeJS.Timeout | null = null
constructor(private config: ScrollSyncConfig) {}
syncScroll(sourcePane: HTMLElement, scrollTop: number): void {
if (this.isScrolling) return
this.isScrolling = true
switch (this.config.mode) {
case 'verse':
this.syncByVerse(sourcePane, scrollTop)
break
case 'pixel':
this.syncByPixel(sourcePane, scrollTop)
break
case 'paragraph':
this.syncByParagraph(sourcePane, scrollTop)
break
}
// Reset scrolling flag after brief delay
clearTimeout(this.scrollTimeout)
this.scrollTimeout = setTimeout(() => {
this.isScrolling = false
}, 100)
}
private syncByVerse(sourcePane: HTMLElement, scrollTop: number): void {
// Find which verse is at the top of source pane
const visibleVerse = this.getVisibleVerseNumber(sourcePane, scrollTop)
// Scroll other panes to show the same verse at top
this.panes.forEach(pane => {
if (pane === sourcePane) return
const targetVerse = pane.querySelector(`[data-verse="${visibleVerse}"]`)
if (targetVerse) {
pane.scrollTo({
top: (targetVerse as HTMLElement).offsetTop - 100,
behavior: 'smooth'
})
}
})
}
private syncByPixel(sourcePane: HTMLElement, scrollTop: number): void {
// Calculate scroll percentage
const scrollHeight = sourcePane.scrollHeight - sourcePane.clientHeight
const scrollPercent = scrollTop / scrollHeight
// Apply same percentage to other panes
this.panes.forEach(pane => {
if (pane === sourcePane) return
const targetScrollHeight = pane.scrollHeight - pane.clientHeight
const targetScrollTop = targetScrollHeight * scrollPercent
pane.scrollTo({
top: targetScrollTop,
behavior: 'smooth'
})
})
}
private getVisibleVerseNumber(pane: HTMLElement, scrollTop: number): number {
const verses = Array.from(pane.querySelectorAll('[data-verse]'))
const viewportTop = scrollTop + 100 // Offset for header
for (const verse of verses) {
const verseTop = (verse as HTMLElement).offsetTop
if (verseTop >= viewportTop) {
return parseInt(verse.getAttribute('data-verse') || '1')
}
}
return 1
}
}
```
### 4. Version Selector Per Pane
```typescript
interface VersionSelectorProps {
paneId: string
currentVersionId: string
onVersionChange: (versionId: string) => void
position: 'top' | 'bottom'
compact?: boolean
}
const VersionSelector: React.FC<VersionSelectorProps> = ({
paneId,
currentVersionId,
onVersionChange,
position,
compact = false
}) => {
const [versions, setVersions] = useState<BibleVersion[]>([])
const [search, setSearch] = useState('')
useEffect(() => {
// Load available versions
fetch('/api/bible/versions')
.then(r => r.json())
.then(data => setVersions(data.versions))
}, [])
const filteredVersions = versions.filter(v =>
v.name.toLowerCase().includes(search.toLowerCase()) ||
v.abbreviation.toLowerCase().includes(search.toLowerCase())
)
return (
<Box className={`version-selector ${position}`}>
<FormControl fullWidth size={compact ? 'small' : 'medium'}>
<Select
value={currentVersionId}
onChange={(e) => onVersionChange(e.target.value)}
renderValue={(value) => {
const version = versions.find(v => v.id === value)
return version?.abbreviation || 'Select Version'
}}
>
<Box sx={{ p: 1 }}>
<TextField
placeholder="Search versions..."
size="small"
fullWidth
value={search}
onChange={(e) => setSearch(e.target.value)}
onClick={(e) => e.stopPropagation()}
/>
</Box>
<Divider />
{filteredVersions.map(version => (
<MenuItem key={version.id} value={version.id}>
<Box>
<Typography variant="body2" fontWeight="600">
{version.abbreviation}
</Typography>
<Typography variant="caption" color="text.secondary">
{version.name} ({version.language})
</Typography>
</Box>
</MenuItem>
))}
</Select>
</FormControl>
</Box>
)
}
```
### 5. Verse Alignment Highlighting
```typescript
interface AlignmentConfig {
enabled: boolean
highlightMode: 'hover' | 'focus' | 'always' | 'none'
color: string
showConnectors: boolean // Lines between aligned verses
}
// Highlight same verse across all panes
const VerseAlignmentHighlighter: React.FC = () => {
const { panes, alignmentConfig } = useParallelView()
const [hoveredVerse, setHoveredVerse] = useState<number | null>(null)
useEffect(() => {
if (!alignmentConfig.enabled || alignmentConfig.highlightMode === 'none') {
return
}
const handleVerseHover = (e: MouseEvent) => {
const verseElement = (e.target as HTMLElement).closest('[data-verse]')
if (verseElement) {
const verseNum = parseInt(verseElement.getAttribute('data-verse') || '0')
setHoveredVerse(verseNum)
} else {
setHoveredVerse(null)
}
}
document.addEventListener('mouseover', handleVerseHover)
return () => document.removeEventListener('mouseover', handleVerseHover)
}, [alignmentConfig])
useEffect(() => {
if (hoveredVerse === null) {
// Remove all highlights
document.querySelectorAll('.verse-aligned').forEach(el => {
el.classList.remove('verse-aligned')
})
return
}
// Highlight verse in all panes
panes.forEach(pane => {
const verseElements = document.querySelectorAll(
`#pane-${pane.id} [data-verse="${hoveredVerse}"]`
)
verseElements.forEach(el => el.classList.add('verse-aligned'))
})
}, [hoveredVerse, panes])
return null
}
// CSS
.verse-aligned {
background-color: rgba(var(--primary-rgb), 0.1);
border-left: 3px solid var(--primary-color);
padding-left: 8px;
margin-left: -11px;
transition: all 0.2s ease;
}
```
### 6. Diff View for Text Differences
```typescript
interface DiffConfig {
enabled: boolean
compareAgainst: string // Pane ID to use as reference
diffMode: 'word' | 'phrase' | 'verse'
highlightStyle: 'color' | 'underline' | 'background' | 'strikethrough'
showSimilarity: boolean // Show % similarity score
}
// Simple word-level diff
function calculateDiff(text1: string, text2: string): DiffResult[] {
const words1 = text1.split(/\s+/)
const words2 = text2.split(/\s+/)
const diff: DiffResult[] = []
// Simple longest common subsequence approach
let i = 0, j = 0
while (i < words1.length || j < words2.length) {
if (words1[i] === words2[j]) {
diff.push({ type: 'same', text: words1[i] })
i++
j++
} else {
// Check if word exists ahead
const indexInText2 = words2.slice(j).indexOf(words1[i])
const indexInText1 = words1.slice(i).indexOf(words2[j])
if (indexInText2 !== -1 && (indexInText1 === -1 || indexInText2 < indexInText1)) {
// Word missing in text1
diff.push({ type: 'added', text: words2[j] })
j++
} else if (indexInText1 !== -1) {
// Word missing in text2
diff.push({ type: 'removed', text: words1[i] })
i++
} else {
// Different words
diff.push({ type: 'changed', text1: words1[i], text2: words2[j] })
i++
j++
}
}
}
return diff
}
interface DiffResult {
type: 'same' | 'added' | 'removed' | 'changed'
text?: string
text1?: string
text2?: string
}
// Component to render diff
const DiffHighlightedVerse: React.FC<{
verseText: string
referenceText: string
config: DiffConfig
}> = ({ verseText, referenceText, config }) => {
if (!config.enabled) {
return <span>{verseText}</span>
}
const diff = calculateDiff(referenceText, verseText)
return (
<span>
{diff.map((part, index) => {
if (part.type === 'same') {
return <span key={index}>{part.text} </span>
} else if (part.type === 'added') {
return (
<mark key={index} className="diff-added">
{part.text}{' '}
</mark>
)
} else if (part.type === 'removed') {
return (
<del key={index} className="diff-removed">
{part.text}{' '}
</del>
)
} else if (part.type === 'changed') {
return (
<mark key={index} className="diff-changed">
{part.text2}{' '}
</mark>
)
}
})}
</span>
)
}
```
### 7. Quick Swap Versions
```typescript
// Allow swapping versions between panes
const SwapVersionsButton: React.FC<{
pane1Id: string
pane2Id: string
}> = ({ pane1Id, pane2Id }) => {
const { panes, updatePane } = useParallelView()
const handleSwap = () => {
const pane1 = panes.find(p => p.id === pane1Id)
const pane2 = panes.find(p => p.id === pane2Id)
if (pane1 && pane2) {
updatePane(pane1Id, { versionId: pane2.versionId })
updatePane(pane2Id, { versionId: pane1.versionId })
}
}
return (
<IconButton
onClick={handleSwap}
size="small"
title="Swap versions"
sx={{
position: 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
bgcolor: 'background.paper',
boxShadow: 2,
'&:hover': { boxShadow: 4 }
}}
>
<SwapHorizIcon />
</IconButton>
)
}
```
### 8. Column Width Adjustment
```typescript
interface ResizablePane {
id: string
minWidth: number // percentage
maxWidth: number
currentWidth: number
}
// Draggable divider between panes
const PaneDivider: React.FC<{
leftPaneId: string
rightPaneId: string
}> = ({ leftPaneId, rightPaneId }) => {
const { updatePane } = useParallelView()
const [isDragging, setIsDragging] = useState(false)
const [startX, setStartX] = useState(0)
const [startWidths, setStartWidths] = useState<[number, number]>([50, 50])
const handleMouseDown = (e: React.MouseEvent) => {
setIsDragging(true)
setStartX(e.clientX)
const leftPane = document.getElementById(`pane-${leftPaneId}`)
const rightPane = document.getElementById(`pane-${rightPaneId}`)
if (leftPane && rightPane) {
const leftWidth = (leftPane.offsetWidth / leftPane.parentElement!.offsetWidth) * 100
const rightWidth = (rightPane.offsetWidth / rightPane.parentElement!.offsetWidth) * 100
setStartWidths([leftWidth, rightWidth])
}
}
useEffect(() => {
if (!isDragging) return
const handleMouseMove = (e: MouseEvent) => {
const deltaX = e.clientX - startX
const container = document.querySelector('.parallel-view-container')
if (!container) return
const deltaPercent = (deltaX / container.clientWidth) * 100
const newLeftWidth = Math.max(20, Math.min(80, startWidths[0] + deltaPercent))
const newRightWidth = Math.max(20, Math.min(80, startWidths[1] - deltaPercent))
updatePane(leftPaneId, { width: newLeftWidth })
updatePane(rightPaneId, { width: newRightWidth })
}
const handleMouseUp = () => {
setIsDragging(false)
}
document.addEventListener('mousemove', handleMouseMove)
document.addEventListener('mouseup', handleMouseUp)
return () => {
document.removeEventListener('mousemove', handleMouseMove)
document.removeEventListener('mouseup', handleMouseUp)
}
}, [isDragging, startX, startWidths])
return (
<Box
onMouseDown={handleMouseDown}
className={`pane-divider ${isDragging ? 'dragging' : ''}`}
sx={{
width: '8px',
cursor: 'col-resize',
bgcolor: 'divider',
position: 'relative',
'&:hover': {
bgcolor: 'primary.main',
width: '12px'
},
'&.dragging': {
bgcolor: 'primary.main',
width: '12px'
}
}}
/>
)
}
```
### 9. Independent Highlighting Per Version
```typescript
// Each pane maintains its own highlights
interface PaneHighlights {
paneId: string
highlights: Highlight[]
}
// Store highlights per version in database
model Highlight {
id String @id @default(cuid())
userId String
versionId String // Link to specific Bible version
book String
chapter Int
verse Int
color String
note String?
createdAt DateTime @default(now())
user User @relation(fields: [userId], references: [id])
version BibleVersion @relation(fields: [versionId], references: [id])
@@index([userId, versionId, book, chapter])
}
// Load highlights per pane
const loadPaneHighlights = async (
paneId: string,
versionId: string,
book: string,
chapter: number
): Promise<Highlight[]> => {
const response = await fetch(
`/api/highlights?versionId=${versionId}&book=${book}&chapter=${chapter}`
)
return response.json()
}
```
---
## 🏗️ Technical Implementation
### File Structure
```
/components/bible-reader/
├── parallel-view/
│ ├── ParallelViewProvider.tsx # Context provider
│ ├── ParallelViewContainer.tsx # Main container
│ ├── Pane.tsx # Individual pane
│ ├── PaneDivider.tsx # Resizable divider
│ ├── VersionSelector.tsx # Version picker per pane
│ ├── LayoutSelector.tsx # Layout switcher
│ ├── ScrollSynchronizer.tsx # Scroll sync logic
│ ├── VerseAlignmentHighlighter.tsx # Verse highlighting
│ ├── DiffView.tsx # Text difference view
│ ├── SwapControl.tsx # Version swapping
│ └── hooks/
│ ├── useParallelView.ts # Main hook
│ ├── useScrollSync.ts # Scroll synchronization
│ ├── usePaneResize.ts # Resize logic
│ └── useVerseAlignment.ts # Alignment logic
└── reader.tsx # Updated main reader
```
### Context Provider
```typescript
// ParallelViewProvider.tsx
interface ParallelViewContextType {
// State
enabled: boolean
layout: LayoutConfig
panes: PaneConfig[]
scrollSync: ScrollSyncConfig
alignmentConfig: AlignmentConfig
diffConfig: DiffConfig
// Actions
toggleParallelView: () => void
addPane: (config: Partial<PaneConfig>) => void
removePane: (paneId: string) => void
updatePane: (paneId: string, updates: Partial<PaneConfig>) => void
setLayout: (layout: PaneLayout) => void
updateScrollSync: (config: Partial<ScrollSyncConfig>) => void
swapVersions: (paneId1: string, paneId2: string) => void
}
export const ParallelViewProvider: React.FC<{
children: React.ReactNode
}> = ({ children }) => {
const [enabled, setEnabled] = useState(false)
const [layout, setLayoutState] = useState<LayoutConfig>(defaultLayout)
const [panes, setPanes] = useState<PaneConfig[]>([])
const [scrollSync, setScrollSync] = useState<ScrollSyncConfig>(defaultScrollSync)
// Load from localStorage
useEffect(() => {
const saved = localStorage.getItem('parallel-view-config')
if (saved) {
const config = JSON.parse(saved)
setEnabled(config.enabled)
setLayoutState(config.layout)
setPanes(config.panes)
setScrollSync(config.scrollSync)
} else {
// Initialize with default 2-pane view
const defaultPanes = [
{ id: 'pane-1', versionId: 'kjv', visible: true, width: 50, locked: false },
{ id: 'pane-2', versionId: 'esv', visible: true, width: 50, locked: false }
]
setPanes(defaultPanes)
}
}, [])
// Save to localStorage
useEffect(() => {
localStorage.setItem('parallel-view-config', JSON.stringify({
enabled,
layout,
panes,
scrollSync
}))
}, [enabled, layout, panes, scrollSync])
const addPane = (config: Partial<PaneConfig>) => {
const newPane: PaneConfig = {
id: `pane-${Date.now()}`,
versionId: config.versionId || 'kjv',
visible: true,
width: 100 / (panes.length + 1),
locked: false,
...config
}
// Adjust existing pane widths
const adjustedPanes = panes.map(p => ({
...p,
width: p.width * (panes.length / (panes.length + 1))
}))
setPanes([...adjustedPanes, newPane])
}
const removePane = (paneId: string) => {
const updatedPanes = panes.filter(p => p.id !== paneId)
// Redistribute widths
const equalWidth = 100 / updatedPanes.length
setPanes(updatedPanes.map(p => ({ ...p, width: equalWidth })))
}
const updatePane = (paneId: string, updates: Partial<PaneConfig>) => {
setPanes(panes.map(p =>
p.id === paneId ? { ...p, ...updates } : p
))
}
const swapVersions = (paneId1: string, paneId2: string) => {
const pane1 = panes.find(p => p.id === paneId1)
const pane2 = panes.find(p => p.id === paneId2)
if (pane1 && pane2) {
updatePane(paneId1, { versionId: pane2.versionId })
updatePane(paneId2, { versionId: pane1.versionId })
}
}
return (
<ParallelViewContext.Provider value={{
enabled,
layout,
panes,
scrollSync,
alignmentConfig,
diffConfig,
toggleParallelView: () => setEnabled(!enabled),
addPane,
removePane,
updatePane,
setLayout: setLayoutState,
updateScrollSync: (config) => setScrollSync({ ...scrollSync, ...config }),
swapVersions
}}>
{children}
</ParallelViewContext.Provider>
)
}
```
### Main Container Component
```typescript
// ParallelViewContainer.tsx
export const ParallelViewContainer: React.FC = () => {
const { enabled, layout, panes, scrollSync } = useParallelView()
const scrollSynchronizer = useRef(new ScrollSynchronizer(scrollSync))
if (!enabled || panes.length === 0) {
return null
}
const visiblePanes = panes.filter(p => p.visible)
const getGridTemplate = () => {
switch (layout.layout) {
case '2-pane-horizontal':
return 'repeat(2, 1fr)'
case '3-pane':
return 'repeat(3, 1fr)'
case '4-pane':
return 'repeat(2, 1fr)'
default:
return '1fr'
}
}
return (
<Box
className="parallel-view-container"
sx={{
display: 'grid',
gridTemplateColumns: getGridTemplate(),
gap: layout.showDividers ? 1 : 0,
height: '100%',
overflow: 'hidden'
}}
>
{visiblePanes.map((pane, index) => (
<React.Fragment key={pane.id}>
<Pane
config={pane}
onScroll={(scrollTop) => {
if (scrollSync.enabled) {
scrollSynchronizer.current.syncScroll(
document.getElementById(`pane-${pane.id}`)!,
scrollTop
)
}
}}
/>
{layout.showDividers && index < visiblePanes.length - 1 && (
<PaneDivider
leftPaneId={visiblePanes[index].id}
rightPaneId={visiblePanes[index + 1].id}
/>
)}
</React.Fragment>
))}
</Box>
)
}
```
---
## 💾 Data Persistence
### LocalStorage Schema
```typescript
interface ParallelViewStorage {
version: number
enabled: boolean
layout: LayoutConfig
panes: PaneConfig[]
scrollSync: ScrollSyncConfig
alignmentConfig: AlignmentConfig
diffConfig: DiffConfig
recentVersionCombinations: string[][] // Track popular combos
}
// Key: 'bible-reader:parallel-view'
```
### User Preferences API
```typescript
// Add to UserPreference model
model UserPreference {
// ... existing fields
parallelViewConfig Json?
favoriteVersionCombinations Json? // [["kjv", "esv"], ["niv", "msg"]]
}
// API endpoint
POST /api/user/preferences/parallel-view
Body: ParallelViewStorage
```
---
## 📅 Implementation Timeline
### Week 1: Core Functionality
**Day 1-2: Foundation**
- [ ] Create context provider
- [ ] Build basic 2-pane layout
- [ ] Implement version selector per pane
- [ ] Add layout switcher (1/2/3 panes)
**Day 3-4: Scroll Sync**
- [ ] Implement scroll synchronizer
- [ ] Add verse-based sync
- [ ] Add pixel-based sync
- [ ] Test smooth scrolling
**Day 5: Resizing & Controls**
- [ ] Build resizable dividers
- [ ] Add width adjustment
- [ ] Implement swap versions
- [ ] Test on different screen sizes
**Deliverable:** Working parallel view with basic features
### Week 2: Advanced Features & Polish
**Day 1-2: Alignment & Diff**
- [ ] Implement verse alignment highlighting
- [ ] Build diff view
- [ ] Add similarity calculations
- [ ] Test with various translations
**Day 3-4: Mobile & Responsive**
- [ ] Design mobile layout (tabs)
- [ ] Implement swipe navigation
- [ ] Optimize for tablets
- [ ] Test touch gestures
**Day 5: Polish & Testing**
- [ ] Independent highlighting per pane
- [ ] Performance optimization
- [ ] Bug fixes
- [ ] Documentation
**Deliverable:** Production-ready parallel Bible view
---
## 🚀 Deployment Plan
### Pre-Launch Checklist
- [ ] All layouts tested (2/3/4 pane)
- [ ] Scroll sync working smoothly
- [ ] Mobile responsive design complete
- [ ] Performance benchmarks met (<100ms lag)
- [ ] Accessibility audit passed
- [ ] Cross-browser testing complete
- [ ] User documentation created
### Rollout Strategy
1. **Beta (Week 1)**: 10% of users, 2-pane only
2. **Staged (Week 2)**: 50% of users, all layouts
3. **Full (Week 3)**: 100% of users
---
## 📝 Notes & Considerations
### Performance
- Use virtual scrolling for long chapters
- Debounce scroll sync (avoid jank)
- Lazy load panes not in viewport
- Cache rendered verses
- Monitor memory usage with multiple panes
### Accessibility
- Maintain keyboard navigation across panes
- Screen reader support for pane switching
- Focus management between panes
- ARIA labels for all controls
---
**Document Version:** 1.0
**Last Updated:** 2025-10-13
**Owner:** Development Team
**Status:** Ready for Implementation