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>
801 lines
22 KiB
Markdown
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
|