This repository has been archived on 2026-07-21. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
cloud-reader/frontend/src/components/reader/ReadingSettingsPanel.tsx
T
2026-06-04 06:42:27 -05:00

252 lines
7.9 KiB
TypeScript

/**
* ReadingSettingsPanel — slide-in drawer from the right for customizing
* the reading experience: theme, font, sizing, orientation.
*/
import type { KeyboardEvent as ReactKeyboardEvent } from "react";
import { useTranslation } from "react-i18n-lite";
import type {
FontFamily,
OrientationLock,
ReadingSettings,
ThemePreset,
} from "../../types/reader";
import type { SettingsPersistMode } from "../../hooks/useReadingSettings";
import { PanelCloseButton } from "./PanelCloseButton";
interface ReadingSettingsPanelProps {
format?: "epub" | "pdf";
settings: ReadingSettings;
pdfScale?: number;
onPdfScaleChange?: (scale: number) => void;
isOpen: boolean;
onClose: () => void;
onUpdate: (
partial: Partial<ReadingSettings>,
persist?: SettingsPersistMode,
) => void;
onFlush: () => Promise<void>;
}
const THEME_OPTIONS: { value: ThemePreset; labelKey: string }[] = [
{ value: "sepia", labelKey: "reader.themeSepia" },
{ value: "dark", labelKey: "reader.themeDark" },
{ value: "light", labelKey: "reader.themeLight" },
{ value: "paper", labelKey: "reader.themePaper" },
];
const FONT_OPTIONS: { value: FontFamily; labelKey: string }[] = [
{ value: "sans-serif", labelKey: "reader.fontSans" },
{ value: "serif", labelKey: "reader.fontSerif" },
{ value: "monospace", labelKey: "reader.fontMonospace" },
];
const ORIENTATION_OPTIONS: { value: OrientationLock; labelKey: string }[] = [
{ value: "auto", labelKey: "reader.orientationAuto" },
{ value: "portrait", labelKey: "reader.orientationPortrait" },
{ value: "landscape", labelKey: "reader.orientationLandscape" },
];
export default function ReadingSettingsPanel({
format = "epub",
settings,
pdfScale = 1.25,
onPdfScaleChange,
isOpen,
onClose,
onUpdate,
onFlush,
}: ReadingSettingsPanelProps) {
const { t } = useTranslation();
const handleClose = () => {
void onFlush();
onClose();
};
const handleSlider = (key: keyof ReadingSettings, value: number) => {
onUpdate({ [key]: value } as Partial<ReadingSettings>, "debounced");
};
const handleDiscrete = (key: keyof ReadingSettings, value: string | number) => {
onUpdate({ [key]: value } as Partial<ReadingSettings>, "immediate");
};
return (
<>
{isOpen && (
<div
className="settings-overlay"
onClick={handleClose}
onKeyDown={(e: ReactKeyboardEvent) => {
if (e.key === "Escape") handleClose();
}}
role="presentation"
/>
)}
<aside
className={`settings-drawer ${isOpen ? "settings-drawer--open" : ""}`}
>
<div className="settings-header">
<h2 className="settings-title">{t("reader.settingsTitle")}</h2>
<PanelCloseButton
className="settings-close-btn"
onClick={handleClose}
ariaLabel={t("reader.closeSettingsAria")}
/>
</div>
<div className="settings-body">
<section className="settings-section">
<h3 className="settings-section-title">{t("reader.theme")}</h3>
<div className="theme-grid">
{THEME_OPTIONS.map((opt) => (
<button
key={opt.value}
type="button"
className={`theme-btn ${
settings.theme === opt.value ? "theme-btn--active" : ""
}`}
onClick={() => handleDiscrete("theme", opt.value)}
>
{t(opt.labelKey)}
</button>
))}
</div>
</section>
{format === "pdf" && onPdfScaleChange && (
<section className="settings-section">
<h3 className="settings-section-title">
{t("reader.pdfZoom", { value: String(Math.round(pdfScale * 100)) })}
</h3>
<input
type="range"
min="0.75"
max="2.5"
step="0.05"
value={pdfScale}
onChange={(e) => onPdfScaleChange(Number(e.target.value))}
className="settings-slider"
aria-label={t("reader.pdfZoomAria")}
/>
</section>
)}
{format === "epub" && (
<>
<section className="settings-section">
<h3 className="settings-section-title">{t("reader.font")}</h3>
<div className="font-grid">
{FONT_OPTIONS.map((opt) => (
<button
key={opt.value}
type="button"
className={`font-btn ${
settings.font_family === opt.value ? "font-btn--active" : ""
}`}
onClick={() => handleDiscrete("font_family", opt.value)}
>
{t(opt.labelKey)}
</button>
))}
</div>
</section>
<section className="settings-section">
<h3 className="settings-section-title">
{t("reader.fontSize", { size: String(settings.font_size) })}
</h3>
<input
type="range"
min="12"
max="32"
value={settings.font_size}
onChange={(e) =>
handleSlider("font_size", Number(e.target.value))
}
className="settings-slider"
aria-label={t("reader.fontSizeAria")}
/>
</section>
<section className="settings-section">
<h3 className="settings-section-title">
{t("reader.lineHeight", { value: settings.line_height.toFixed(1) })}
</h3>
<input
type="range"
min="1.2"
max="2.0"
step="0.1"
value={settings.line_height}
onChange={(e) =>
handleSlider("line_height", Number(e.target.value))
}
className="settings-slider"
aria-label={t("reader.lineHeightAria")}
/>
</section>
<section className="settings-section">
<h3 className="settings-section-title">
{t("reader.margins", { value: String(settings.margin_width) })}
</h3>
<input
type="range"
min="8"
max="48"
value={settings.margin_width}
onChange={(e) =>
handleSlider("margin_width", Number(e.target.value))
}
className="settings-slider"
aria-label={t("reader.marginWidthAria")}
/>
</section>
</>
)}
<section className="settings-section">
<h3 className="settings-section-title">
{t("reader.brightness", { value: String(settings.brightness) })}
</h3>
<input
type="range"
min="0"
max="100"
value={settings.brightness}
onChange={(e) =>
handleSlider("brightness", Number(e.target.value))
}
className="settings-slider"
aria-label={t("reader.brightnessAria")}
/>
</section>
<section className="settings-section">
<h3 className="settings-section-title">{t("reader.orientation")}</h3>
<div className="orientation-grid">
{ORIENTATION_OPTIONS.map((opt) => (
<button
key={opt.value}
type="button"
className={`orientation-btn ${
settings.orientation_lock === opt.value
? "orientation-btn--active"
: ""
}`}
onClick={() => handleDiscrete("orientation_lock", opt.value)}
>
{t(opt.labelKey)}
</button>
))}
</div>
</section>
</div>
</aside>
</>
);
}