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:
800
CUSTOM_FONTS_DYSLEXIA_SUPPORT_PLAN.md
Normal file
800
CUSTOM_FONTS_DYSLEXIA_SUPPORT_PLAN.md
Normal file
@@ -0,0 +1,800 @@
|
||||
# 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
|
||||
Reference in New Issue
Block a user