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

22 KiB

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

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

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

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

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

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

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

// 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

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