Files
biblical-guide.com/CUSTOM_FONTS_DYSLEXIA_SUPPORT_PLAN.md
Andrei 9b5c0ed8bb 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>
2025-11-11 20:38:01 +00:00

801 lines
22 KiB
Markdown

# Custom Fonts & Dyslexia Support - Implementation Plan
## 📋 Overview
Implement comprehensive font customization and dyslexia-friendly features to improve readability for all users, with special accommodations for those with reading difficulties or visual processing challenges.
**Status:** Planning Phase
**Priority:** 🟡 Medium
**Estimated Time:** 1 week (40 hours)
**Target Completion:** TBD
---
## 🎯 Goals & Objectives
### Primary Goals
1. Provide extensive font customization options
2. Integrate dyslexia-friendly fonts and features
3. Enable color overlay filters for visual comfort
4. Support custom font uploads
5. Offer letter/word spacing adjustments
### User Value Proposition
- **For dyslexic readers**: Specialized fonts and spacing
- **For visually impaired**: High contrast and large text options
- **For personal preference**: Complete customization
- **For comfort**: Reduce eye strain
- **For accessibility**: WCAG AAA compliance
---
## ✨ Feature Specifications
### 1. Font Configuration
```typescript
interface FontConfig {
// Font Selection
fontFamily: string
customFontUrl?: string // For uploaded fonts
// Size
fontSize: number // 12-32px
fontSizePreset: 'small' | 'medium' | 'large' | 'extra-large' | 'custom'
// Weight & Style
fontWeight: number // 300-900
fontStyle: 'normal' | 'italic'
// Spacing
letterSpacing: number // -2 to 10px
wordSpacing: number // -5 to 20px
lineHeight: number // 1.0 - 3.0
paragraphSpacing: number // 0-40px
// Dyslexia Features
isDyslexiaMode: boolean
dyslexiaFontSize: number // Usually 14-18pt for dyslexia
dyslexiaSpacing: 'normal' | 'wide' | 'extra-wide'
boldFirstLetters: boolean // Bionic reading style
// Visual Aids
colorOverlay: string | null // Tinted overlay
overlayOpacity: number // 0-100%
highContrast: boolean
underlineLinks: boolean
// Advanced
textTransform: 'none' | 'uppercase' | 'lowercase' | 'capitalize'
textDecoration: 'none' | 'underline' | 'overline'
}
// Available font families
const FONT_FAMILIES = {
standard: [
{ name: 'System Default', value: 'system-ui, -apple-system' },
{ name: 'Arial', value: 'Arial, sans-serif' },
{ name: 'Georgia', value: 'Georgia, serif' },
{ name: 'Times New Roman', value: '"Times New Roman", serif' },
{ name: 'Verdana', value: 'Verdana, sans-serif' },
{ name: 'Courier New', value: '"Courier New", monospace' }
],
readable: [
{ name: 'Open Sans', value: '"Open Sans", sans-serif' },
{ name: 'Lora', value: 'Lora, serif' },
{ name: 'Merriweather', value: 'Merriweather, serif' },
{ name: 'Roboto', value: 'Roboto, sans-serif' },
{ name: 'Source Sans Pro', value: '"Source Sans Pro", sans-serif' }
],
dyslexiaFriendly: [
{
name: 'OpenDyslexic',
value: 'OpenDyslexic, sans-serif',
url: '/fonts/OpenDyslexic-Regular.woff2',
description: 'Specially designed with weighted bottoms to prevent letter rotation'
},
{
name: 'Lexend',
value: 'Lexend, sans-serif',
url: 'https://fonts.googleapis.com/css2?family=Lexend:wght@300;400;500;600;700',
description: 'Variable font designed to reduce visual stress'
},
{
name: 'Comic Sans MS',
value: '"Comic Sans MS", cursive',
description: 'Often recommended for dyslexia due to unique letter shapes'
},
{
name: 'Dyslexie',
value: 'Dyslexie, sans-serif',
url: '/fonts/Dyslexie-Regular.woff2',
description: 'Premium font designed by a dyslexic designer',
isPremium: true
}
]
}
```
### 2. Font Selector Component
```typescript
const FontSelector: React.FC<{
config: FontConfig
onChange: (config: Partial<FontConfig>) => void
}> = ({ config, onChange }) => {
const [activeCategory, setActiveCategory] = useState<'standard' | 'readable' | 'dyslexiaFriendly'>('standard')
const [previewText, setPreviewText] = useState('In the beginning God created the heaven and the earth.')
return (
<Box>
<Typography variant="h6" gutterBottom>
Font Selection
</Typography>
{/* Category Tabs */}
<Tabs value={activeCategory} onChange={(_, v) => setActiveCategory(v)} sx={{ mb: 2 }}>
<Tab label="Standard" value="standard" />
<Tab label="Readable" value="readable" />
<Tab label="Dyslexia-Friendly" value="dyslexiaFriendly" />
</Tabs>
{/* Font List */}
<List>
{FONT_FAMILIES[activeCategory].map(font => (
<ListItem
key={font.value}
button
selected={config.fontFamily === font.value}
onClick={() => onChange({ fontFamily: font.value })}
>
<ListItemText
primary={
<Box display="flex" alignItems="center" gap={1}>
<Typography style={{ fontFamily: font.value }}>
{font.name}
</Typography>
{font.isPremium && (
<Chip label="Premium" size="small" color="primary" />
)}
</Box>
}
secondary={font.description}
/>
<ListItemSecondaryAction>
<IconButton onClick={() => loadFontPreview(font)}>
<VisibilityIcon />
</IconButton>
</ListItemSecondaryAction>
</ListItem>
))}
</List>
{/* Upload Custom Font */}
<Button
fullWidth
variant="outlined"
startIcon={<UploadIcon />}
onClick={() => uploadCustomFont()}
sx={{ mt: 2 }}
>
Upload Custom Font
</Button>
{/* Preview */}
<Paper sx={{ p: 2, mt: 3, bgcolor: 'background.default' }}>
<Typography variant="caption" color="text.secondary" gutterBottom>
Preview
</Typography>
<Typography
style={{
fontFamily: config.fontFamily,
fontSize: `${config.fontSize}px`,
letterSpacing: `${config.letterSpacing}px`,
wordSpacing: `${config.wordSpacing}px`,
lineHeight: config.lineHeight
}}
>
{previewText}
</Typography>
</Paper>
</Box>
)
}
```
### 3. Font Size & Spacing Controls
```typescript
const FontSizeControls: React.FC<{
config: FontConfig
onChange: (config: Partial<FontConfig>) => void
}> = ({ config, onChange }) => {
return (
<Box>
<Typography variant="h6" gutterBottom>
Size & Spacing
</Typography>
{/* Font Size Presets */}
<Box sx={{ mb: 3 }}>
<Typography variant="subtitle2" gutterBottom>
Quick Presets
</Typography>
<ButtonGroup fullWidth>
<Button
variant={config.fontSizePreset === 'small' ? 'contained' : 'outlined'}
onClick={() => onChange({ fontSizePreset: 'small', fontSize: 14 })}
>
Small
</Button>
<Button
variant={config.fontSizePreset === 'medium' ? 'contained' : 'outlined'}
onClick={() => onChange({ fontSizePreset: 'medium', fontSize: 16 })}
>
Medium
</Button>
<Button
variant={config.fontSizePreset === 'large' ? 'contained' : 'outlined'}
onClick={() => onChange({ fontSizePreset: 'large', fontSize: 20 })}
>
Large
</Button>
<Button
variant={config.fontSizePreset === 'extra-large' ? 'contained' : 'outlined'}
onClick={() => onChange({ fontSizePreset: 'extra-large', fontSize: 24 })}
>
Extra Large
</Button>
</ButtonGroup>
</Box>
{/* Custom Font Size */}
<Box sx={{ mb: 3 }}>
<Typography variant="subtitle2" gutterBottom>
Font Size: {config.fontSize}px
</Typography>
<Slider
value={config.fontSize}
onChange={(_, value) => onChange({ fontSize: value as number, fontSizePreset: 'custom' })}
min={12}
max={32}
step={1}
marks={[
{ value: 12, label: '12' },
{ value: 16, label: '16' },
{ value: 20, label: '20' },
{ value: 24, label: '24' },
{ value: 32, label: '32' }
]}
/>
</Box>
{/* Letter Spacing */}
<Box sx={{ mb: 3 }}>
<Typography variant="subtitle2" gutterBottom>
Letter Spacing: {config.letterSpacing}px
</Typography>
<Slider
value={config.letterSpacing}
onChange={(_, value) => onChange({ letterSpacing: value as number })}
min={-2}
max={10}
step={0.5}
marks
/>
</Box>
{/* Word Spacing */}
<Box sx={{ mb: 3 }}>
<Typography variant="subtitle2" gutterBottom>
Word Spacing: {config.wordSpacing}px
</Typography>
<Slider
value={config.wordSpacing}
onChange={(_, value) => onChange({ wordSpacing: value as number })}
min={-5}
max={20}
step={1}
marks
/>
</Box>
{/* Line Height */}
<Box sx={{ mb: 3 }}>
<Typography variant="subtitle2" gutterBottom>
Line Height: {config.lineHeight}
</Typography>
<Slider
value={config.lineHeight}
onChange={(_, value) => onChange({ lineHeight: value as number })}
min={1.0}
max={3.0}
step={0.1}
marks={[
{ value: 1.0, label: '1.0' },
{ value: 1.5, label: '1.5' },
{ value: 2.0, label: '2.0' },
{ value: 2.5, label: '2.5' },
{ value: 3.0, label: '3.0' }
]}
/>
</Box>
{/* Font Weight */}
<Box sx={{ mb: 3 }}>
<Typography variant="subtitle2" gutterBottom>
Font Weight: {config.fontWeight}
</Typography>
<Slider
value={config.fontWeight}
onChange={(_, value) => onChange({ fontWeight: value as number })}
min={300}
max={900}
step={100}
marks={[
{ value: 300, label: 'Light' },
{ value: 400, label: 'Normal' },
{ value: 700, label: 'Bold' },
{ value: 900, label: 'Black' }
]}
/>
</Box>
</Box>
)
}
```
### 4. Dyslexia Mode Settings
```typescript
const DyslexiaSettings: React.FC<{
config: FontConfig
onChange: (config: Partial<FontConfig>) => void
}> = ({ config, onChange }) => {
return (
<Box>
<Typography variant="h6" gutterBottom>
Dyslexia Support
</Typography>
<Alert severity="info" sx={{ mb: 3 }}>
These settings are optimized for readers with dyslexia and reading difficulties.
</Alert>
{/* Enable Dyslexia Mode */}
<FormControlLabel
control={
<Switch
checked={config.isDyslexiaMode}
onChange={(e) => {
const enabled = e.target.checked
onChange({
isDyslexiaMode: enabled,
...(enabled && {
fontFamily: 'OpenDyslexic, sans-serif',
fontSize: 16,
letterSpacing: 1,
wordSpacing: 3,
lineHeight: 1.8
})
})
}}
/>
}
label="Enable Dyslexia Mode"
sx={{ mb: 2 }}
/>
{config.isDyslexiaMode && (
<>
{/* Spacing Presets */}
<FormControl fullWidth sx={{ mb: 3 }}>
<InputLabel>Spacing</InputLabel>
<Select
value={config.dyslexiaSpacing}
onChange={(e) => {
const spacing = e.target.value
let letterSpacing = 0
let wordSpacing = 0
if (spacing === 'wide') {
letterSpacing = 1.5
wordSpacing = 4
} else if (spacing === 'extra-wide') {
letterSpacing = 2.5
wordSpacing = 6
}
onChange({
dyslexiaSpacing: spacing as any,
letterSpacing,
wordSpacing
})
}}
>
<MenuItem value="normal">Normal</MenuItem>
<MenuItem value="wide">Wide (Recommended)</MenuItem>
<MenuItem value="extra-wide">Extra Wide</MenuItem>
</Select>
</FormControl>
{/* Bold First Letters */}
<FormControlLabel
control={
<Switch
checked={config.boldFirstLetters}
onChange={(e) => onChange({ boldFirstLetters: e.target.checked })}
/>
}
label={
<Box>
<Typography>Bold First Letters (Bionic Reading)</Typography>
<Typography variant="caption" color="text.secondary">
Makes the first part of each word bold to guide eye movement
</Typography>
</Box>
}
sx={{ mb: 2 }}
/>
{/* High Contrast */}
<FormControlLabel
control={
<Switch
checked={config.highContrast}
onChange={(e) => onChange({ highContrast: e.target.checked })}
/>
}
label="High Contrast Mode"
sx={{ mb: 2 }}
/>
{/* Underline Links */}
<FormControlLabel
control={
<Switch
checked={config.underlineLinks}
onChange={(e) => onChange({ underlineLinks: e.target.checked })}
/>
}
label="Underline All Links"
/>
</>
)}
</Box>
)
}
```
### 5. Color Overlay Filters
```typescript
const ColorOverlaySettings: React.FC<{
config: FontConfig
onChange: (config: Partial<FontConfig>) => void
}> = ({ config, onChange }) => {
const overlayColors = [
{ name: 'None', color: null },
{ name: 'Yellow', color: '#FFEB3B', description: 'Reduces glare' },
{ name: 'Blue', color: '#2196F3', description: 'Calming effect' },
{ name: 'Green', color: '#4CAF50', description: 'Eye comfort' },
{ name: 'Pink', color: '#E91E63', description: 'Reduces contrast' },
{ name: 'Orange', color: '#FF9800', description: 'Warm tint' },
{ name: 'Purple', color: '#9C27B0', description: 'Reduces brightness' }
]
return (
<Box>
<Typography variant="h6" gutterBottom>
Color Overlay
</Typography>
<Alert severity="info" sx={{ mb: 3 }}>
Color overlays can help reduce visual stress and improve reading comfort.
</Alert>
{/* Overlay Color Selection */}
<Grid container spacing={2} sx={{ mb: 3 }}>
{overlayColors.map(overlay => (
<Grid item xs={6} sm={4} key={overlay.name}>
<Paper
sx={{
p: 2,
textAlign: 'center',
cursor: 'pointer',
border: 2,
borderColor: config.colorOverlay === overlay.color ? 'primary.main' : 'transparent',
bgcolor: overlay.color || 'background.paper',
'&:hover': { boxShadow: 4 }
}}
onClick={() => onChange({ colorOverlay: overlay.color })}
>
<Typography variant="subtitle2" fontWeight="600">
{overlay.name}
</Typography>
{overlay.description && (
<Typography variant="caption" color="text.secondary">
{overlay.description}
</Typography>
)}
</Paper>
</Grid>
))}
</Grid>
{/* Opacity Control */}
{config.colorOverlay && (
<Box>
<Typography variant="subtitle2" gutterBottom>
Overlay Opacity: {config.overlayOpacity}%
</Typography>
<Slider
value={config.overlayOpacity}
onChange={(_, value) => onChange({ overlayOpacity: value as number })}
min={10}
max={100}
step={5}
marks
/>
</Box>
)}
{/* Preview */}
<Paper
sx={{
p: 3,
mt: 3,
position: 'relative',
bgcolor: 'background.default'
}}
>
{config.colorOverlay && (
<Box
sx={{
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
bgcolor: config.colorOverlay,
opacity: config.overlayOpacity / 100,
pointerEvents: 'none'
}}
/>
)}
<Typography variant="caption" color="text.secondary" gutterBottom>
Preview with overlay
</Typography>
<Typography>
The quick brown fox jumps over the lazy dog. In the beginning God created the heaven and the earth.
</Typography>
</Paper>
</Box>
)
}
```
### 6. Custom Font Upload
```typescript
const CustomFontUpload: React.FC<{
onUpload: (fontUrl: string, fontName: string) => void
}> = ({ onUpload }) => {
const [uploading, setUploading] = useState(false)
const [fontName, setFontName] = useState('')
const handleFileSelect = async (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0]
if (!file) return
// Validate file type
const validTypes = ['.woff', '.woff2', '.ttf', '.otf']
const fileExt = file.name.substring(file.name.lastIndexOf('.')).toLowerCase()
if (!validTypes.includes(fileExt)) {
alert('Please upload a valid font file (.woff, .woff2, .ttf, .otf)')
return
}
setUploading(true)
try {
// Upload to server or cloud storage
const formData = new FormData()
formData.append('font', file)
formData.append('name', fontName || file.name)
const response = await fetch('/api/fonts/upload', {
method: 'POST',
body: formData
})
const data = await response.json()
if (data.success) {
onUpload(data.fontUrl, data.fontName)
}
} catch (error) {
console.error('Font upload failed:', error)
} finally {
setUploading(false)
}
}
return (
<Dialog open onClose={() => {}}>
<DialogTitle>Upload Custom Font</DialogTitle>
<DialogContent>
<Box sx={{ pt: 2 }}>
<TextField
label="Font Name"
value={fontName}
onChange={(e) => setFontName(e.target.value)}
fullWidth
sx={{ mb: 3 }}
/>
<Button
component="label"
variant="outlined"
fullWidth
startIcon={<UploadIcon />}
disabled={uploading}
>
{uploading ? 'Uploading...' : 'Select Font File'}
<input
type="file"
hidden
accept=".woff,.woff2,.ttf,.otf"
onChange={handleFileSelect}
/>
</Button>
<Alert severity="info" sx={{ mt: 2 }}>
Supported formats: WOFF, WOFF2, TTF, OTF
</Alert>
</Box>
</DialogContent>
</Dialog>
)
}
```
### 7. Apply Font Configuration
```typescript
// Apply configuration to reader
const applyFontConfig = (config: FontConfig) => {
const readerElement = document.querySelector('.bible-reader-content')
if (!readerElement) return
const styles = {
fontFamily: config.fontFamily,
fontSize: `${config.fontSize}px`,
fontWeight: config.fontWeight,
fontStyle: config.fontStyle,
letterSpacing: `${config.letterSpacing}px`,
wordSpacing: `${config.wordSpacing}px`,
lineHeight: config.lineHeight,
textTransform: config.textTransform,
textDecoration: config.textDecoration
}
Object.assign(readerElement.style, styles)
// Apply high contrast
if (config.highContrast) {
readerElement.classList.add('high-contrast')
} else {
readerElement.classList.remove('high-contrast')
}
// Apply color overlay
if (config.colorOverlay) {
const overlay = document.createElement('div')
overlay.className = 'color-overlay'
overlay.style.backgroundColor = config.colorOverlay
overlay.style.opacity = (config.overlayOpacity / 100).toString()
readerElement.prepend(overlay)
}
}
// CSS for high contrast mode
const highContrastStyles = `
.high-contrast {
background-color: #000 !important;
color: #fff !important;
}
.high-contrast .verse-number {
color: #ffeb3b !important;
}
.high-contrast a {
color: #00bcd4 !important;
text-decoration: underline !important;
}
`
```
---
## 🗄️ Database Schema
```prisma
model FontPreference {
id String @id @default(cuid())
userId String @unique
user User @relation(fields: [userId], references: [id])
fontFamily String @default("system-ui")
customFontUrl String?
fontSize Int @default(16)
fontWeight Int @default(400)
letterSpacing Float @default(0)
wordSpacing Float @default(0)
lineHeight Float @default(1.6)
isDyslexiaMode Boolean @default(false)
dyslexiaSpacing String @default("normal")
boldFirstLetters Boolean @default(false)
colorOverlay String?
overlayOpacity Int @default(30)
highContrast Boolean @default(false)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model CustomFont {
id String @id @default(cuid())
userId String
user User @relation(fields: [userId], references: [id])
name String
url String
format String // woff, woff2, ttf, otf
fileSize Int
createdAt DateTime @default(now())
@@index([userId])
}
```
---
## 📅 Implementation Timeline
### Week 1
**Day 1-2:** Foundation
- [ ] Font selector component
- [ ] Size/spacing controls
- [ ] Preview functionality
**Day 3:** Dyslexia Features
- [ ] Dyslexia mode settings
- [ ] OpenDyslexic/Lexend integration
- [ ] Bionic reading formatter
**Day 4:** Visual Aids
- [ ] Color overlay system
- [ ] High contrast mode
- [ ] Accessibility testing
**Day 5:** Polish & Testing
- [ ] Custom font upload
- [ ] Performance optimization
- [ ] Cross-browser testing
- [ ] Documentation
---
**Document Version:** 1.0
**Last Updated:** 2025-10-13
**Status:** Ready for Implementation