import { useEffect, useRef, useState } from "react"; import type { ReactNode } from "react"; import { useBook } from "../store/useBook"; import { listSystemFonts } from "../ipc"; import { type BookFonts, type FontRef, BUNDLED_FONTS, DEFAULT_FONTS, FONT_PAIRINGS, decodeRef, encodeRef, fontStack, pairingFor, } from "../model/fonts"; import { useEscapeLayer } from "../escape"; import { useFocusTrap } from "../focus"; import { Icon } from "./Icon"; const LANGUAGES = [ { value: "en", label: "English" }, { value: "es", label: "Spanish" }, { value: "fr", label: "French" }, { value: "de", label: "German" }, { value: "it", label: "Italian" }, { value: "pt", label: "Portuguese" }, ]; let systemFontCache: string[] | null = null; interface Draft { title: string; subtitle: string; author: string; isbn: string; language: string; bleed: boolean; fonts: BookFonts; } function Field({ label, children }: { label: string; children: ReactNode }) { return ( {label} {children} ); } function FontSelect({ value, system, onChange }: { value: FontRef; system: string[]; onChange: (ref: FontRef) => void }) { const extra = value.kind === "system" && !system.includes(value.family) ? [value.family] : []; return ( onChange(decodeRef(e.target.value))}> {BUNDLED_FONTS.map((f) => ( {f.label} ))} {(system.length > 0 || extra.length > 0) && ( {[...extra, ...system].map((name) => ( {name} ))} )} ); } export function Settings({ onClose, onSave }: { onClose: () => void; onSave: () => void }) { const book = useBook((s) => s.book); const setMetadata = useBook((s) => s.setMetadata); const setSettings = useBook((s) => s.setSettings); const [draft, setDraft] = useState(() => ({ title: book?.metadata.title ?? "", subtitle: book?.metadata.subtitle ?? "", author: book?.metadata.author ?? "", isbn: book?.metadata.isbn ?? "", language: book?.metadata.language ?? "en", bleed: book?.settings.bleed ?? true, fonts: book?.settings.fonts ?? DEFAULT_FONTS, })); const [system, setSystem] = useState(() => systemFontCache ?? []); const [advanced, setAdvanced] = useState(() => pairingFor(draft.fonts) === null); const panelRef = useRef(null); const titleRef = useRef(null); useEffect(() => { titleRef.current?.focus(); titleRef.current?.select(); }, []); useEffect(() => { if (systemFontCache) return; listSystemFonts().then((fonts) => { systemFontCache = fonts; setSystem(fonts); }); }, []); useEscapeLayer(true, onClose); useFocusTrap(panelRef); if (!book) return null; const set = (patch: Partial) => setDraft((d) => ({ ...d, ...patch })); const setFont = (slot: "body" | "heading", ref: FontRef) => set({ fonts: { ...draft.fonts, [slot]: ref } }); const activePreset = pairingFor(draft.fonts); const usesSystem = draft.fonts.body.kind === "system" || draft.fonts.heading.kind === "system"; const save = () => { const { bleed, fonts, ...metadata } = draft; setMetadata(metadata); setSettings({ bleed, fonts }); onSave(); onClose(); }; return ( e.stopPropagation()}> Book setup set({ title: e.target.value })} /> set({ subtitle: e.target.value })} /> set({ author: e.target.value })} /> set({ isbn: e.target.value })} /> set({ language: e.target.value })}> {LANGUAGES.map((l) => ( {l.label} ))} Typography {FONT_PAIRINGS.map((p) => ( set({ fonts: { body: p.body, heading: p.heading } })} > Ag {p.label} ))} setAdvanced((v) => !v)}> {advanced ? "Hide custom fonts" : "Customize fonts"} {advanced && ( <> setFont("body", ref)} /> setFont("heading", ref)} /> {usesSystem && ( System fonts preview here but may not embed in exported PDF or EPUB, so they can look different on other devices. )} > )} Chapter One She read the first line twice, and the whole quiet house seemed to lean in to listen. set({ bleed: e.target.checked })} /> Add bleed for full-page images (print) Save ); }
System fonts preview here but may not embed in exported PDF or EPUB, so they can look different on other devices.