216 lines
6.3 KiB
TypeScript
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`
|
|
}
|