2026-01-15 11:31:09 +01:00

216 lines
6.3 KiB
TypeScript

import { useState, useEffect } from 'react'
import { Button } from './ui'
import { t } from '@/lib/i18n'
import { getReadingModeSettings, saveReadingModeSettings, type ReadingModeSettings } from '@/lib/readingModePreferences'
const DEFAULT_SETTINGS: ReadingModeSettings = {
maxWidth: 'medium',
fontSize: 'medium',
lineHeight: 'normal',
}
interface ReadingModeProps {
children: React.ReactNode
className?: string
}
function useReadingModeState(): {
isActive: boolean
setIsActive: (value: boolean) => void
settings: ReadingModeSettings
setSettings: (settings: ReadingModeSettings) => void
} {
const [isActive, setIsActive] = useState(false)
const [settings, setSettings] = useState<ReadingModeSettings>(DEFAULT_SETTINGS)
useEffect(() => {
const load = async (): Promise<void> => {
try {
const saved = await getReadingModeSettings()
if (saved !== null) {
setSettings(saved)
}
} catch (error) {
console.error('Error loading reading mode settings:', error)
}
}
void load()
}, [])
useEffect(() => {
if (isActive) {
const save = async (): Promise<void> => {
try {
await saveReadingModeSettings(settings)
} catch (error) {
console.error('Error saving reading mode settings:', error)
}
}
void save()
}
}, [isActive, settings])
return { isActive, setIsActive, settings, setSettings }
}
export function ReadingMode({ children, className = '' }: ReadingModeProps): React.ReactElement {
const { isActive, setIsActive, settings, setSettings } = useReadingModeState()
const readingModeClasses = isActive ? getReadingModeClasses(settings) : ''
const containerClasses = isActive ? `reading-mode ${readingModeClasses}` : ''
return (
<div className={className}>
<ReadingModeToggle isActive={isActive} onToggle={() => setIsActive(!isActive)} />
{isActive && (
<ReadingModeControls settings={settings} onSettingsChange={setSettings} />
)}
<div className={containerClasses}>
{children}
</div>
</div>
)
}
function ReadingModeToggle({ isActive, onToggle }: { isActive: boolean; onToggle: () => void }): React.ReactElement {
return (
<Button
type="button"
variant="ghost"
size="small"
onClick={onToggle}
aria-label={isActive ? t('readingMode.disable') : t('readingMode.enable')}
className="mb-4"
>
{isActive ? t('readingMode.disable') : t('readingMode.enable')}
</Button>
)
}
function ReadingModeControls({
settings,
onSettingsChange,
}: {
settings: ReadingModeSettings
onSettingsChange: (settings: ReadingModeSettings) => void
}): React.ReactElement {
return (
<div className="mb-4 p-4 bg-cyber-dark border border-neon-cyan/30 rounded-lg space-y-4">
<WidthControl settings={settings} onSettingsChange={onSettingsChange} />
<FontSizeControl settings={settings} onSettingsChange={onSettingsChange} />
<LineHeightControl settings={settings} onSettingsChange={onSettingsChange} />
</div>
)
}
function WidthControl({
settings,
onSettingsChange,
}: {
settings: ReadingModeSettings
onSettingsChange: (settings: ReadingModeSettings) => void
}): React.ReactElement {
const widths: ReadingModeSettings['maxWidth'][] = ['narrow', 'medium', 'wide', 'full']
return (
<div>
<label className="block text-sm font-medium text-neon-cyan mb-2">
{t('readingMode.maxWidth')}
</label>
<div className="flex gap-2">
{widths.map((width) => (
<Button
key={width}
type="button"
variant={settings.maxWidth === width ? 'primary' : 'secondary'}
size="small"
onClick={() => onSettingsChange({ ...settings, maxWidth: width })}
>
{t(`readingMode.maxWidth.${width}`)}
</Button>
))}
</div>
</div>
)
}
function FontSizeControl({
settings,
onSettingsChange,
}: {
settings: ReadingModeSettings
onSettingsChange: (settings: ReadingModeSettings) => void
}): React.ReactElement {
const sizes: ReadingModeSettings['fontSize'][] = ['small', 'medium', 'large', 'xlarge']
return (
<div>
<label className="block text-sm font-medium text-neon-cyan mb-2">
{t('readingMode.fontSize')}
</label>
<div className="flex gap-2">
{sizes.map((size) => (
<Button
key={size}
type="button"
variant={settings.fontSize === size ? 'primary' : 'secondary'}
size="small"
onClick={() => onSettingsChange({ ...settings, fontSize: size })}
>
{t(`readingMode.fontSize.${size}`)}
</Button>
))}
</div>
</div>
)
}
function LineHeightControl({
settings,
onSettingsChange,
}: {
settings: ReadingModeSettings
onSettingsChange: (settings: ReadingModeSettings) => void
}): React.ReactElement {
const heights: ReadingModeSettings['lineHeight'][] = ['tight', 'normal', 'relaxed']
return (
<div>
<label className="block text-sm font-medium text-neon-cyan mb-2">
{t('readingMode.lineHeight')}
</label>
<div className="flex gap-2">
{heights.map((height) => (
<Button
key={height}
type="button"
variant={settings.lineHeight === height ? 'primary' : 'secondary'}
size="small"
onClick={() => onSettingsChange({ ...settings, lineHeight: height })}
>
{t(`readingMode.lineHeight.${height}`)}
</Button>
))}
</div>
</div>
)
}
function getReadingModeClasses(settings: ReadingModeSettings): string {
const widthClasses: Record<ReadingModeSettings['maxWidth'], string> = {
narrow: 'max-w-2xl',
medium: 'max-w-3xl',
wide: 'max-w-5xl',
full: 'max-w-full',
}
const fontSizeClasses: Record<ReadingModeSettings['fontSize'], string> = {
small: 'text-sm',
medium: 'text-base',
large: 'text-lg',
xlarge: 'text-xl',
}
const lineHeightClasses: Record<ReadingModeSettings['lineHeight'], string> = {
tight: 'leading-tight',
normal: 'leading-normal',
relaxed: 'leading-relaxed',
}
return `${widthClasses[settings.maxWidth]} ${fontSizeClasses[settings.fontSize]} ${lineHeightClasses[settings.lineHeight]} mx-auto`
}