diff --git a/frontend/src/lib/reader/foliate.d.ts b/frontend/src/lib/reader/foliate-js.d.ts similarity index 93% rename from frontend/src/lib/reader/foliate.d.ts rename to frontend/src/lib/reader/foliate-js.d.ts index cb52e99..ae47cdd 100644 --- a/frontend/src/lib/reader/foliate.d.ts +++ b/frontend/src/lib/reader/foliate-js.d.ts @@ -5,6 +5,10 @@ * ambient declaration is the only candidate — which is the point: svelte-check * never walks the untyped vendored JS. Only the surface Chitai calls is typed. * When you start using a new method, add it here rather than casting to `any`. + * + * The filename must not match a sibling .ts. As foliate.d.ts next to foliate.ts, + * TypeScript takes it for that file's emitted declaration and drops it from the + * program, and every $foliate import fails with TS2307. */ declare module '$foliate/view.js' { /** A TOC entry. `id` is assigned by foliate's `assignIDs`, not by the book. */ diff --git a/frontend/src/lib/reader/settings.ts b/frontend/src/lib/reader/settings.ts new file mode 100644 index 0000000..a058a8a --- /dev/null +++ b/frontend/src/lib/reader/settings.ts @@ -0,0 +1,83 @@ +import { FONT_STACKS } from '$lib/theme/presets'; +import { readerSettingsSchema, type ReaderSettings } from '$lib/schema/reader'; + +export const READER_SETTINGS_STORAGE_KEY = 'chitai:reader-settings'; + +/** Georgia — the app's own title face, and a reasonable default for long prose. */ +export const DEFAULT_READER_SETTINGS: ReaderSettings = { + fontFamily: FONT_STACKS[1].value, + fontSize: 18, + fontWeight: 400, + lineHeight: 1.6, + letterSpacing: 0, + margin: 48, + gap: 6, + maxInlineSize: 720, + maxColumnCount: 2, + flow: 'paginated', + justify: true, + hyphenate: true +}; + +/** Reused from the app theme: system stacks only, so nothing silently falls back. */ +export const READER_FONT_OPTIONS = FONT_STACKS; + +/** Slider bounds, kept next to the schema they mirror. */ +export const READER_BOUNDS = { + fontSize: { min: 12, max: 32, step: 1 }, + fontWeight: { min: 300, max: 700, step: 100 }, + lineHeight: { min: 1, max: 2.5, step: 0.05 }, + letterSpacing: { min: -0.05, max: 0.2, step: 0.01 }, + margin: { min: 0, max: 120, step: 4 }, + gap: { min: 0, max: 15, step: 1 }, + maxInlineSize: { min: 480, max: 1400, step: 20 } +} as const; + +/** + * Reads stored settings, falling back to defaults on anything unusable. + * + * Unknown keys are stripped and missing ones filled, so settings added in a + * later release do not invalidate what a reader already has stored. + */ +export function loadReaderSettings(): ReaderSettings { + try { + const raw = localStorage.getItem(READER_SETTINGS_STORAGE_KEY); + if (!raw) return DEFAULT_READER_SETTINGS; + const parsed = readerSettingsSchema.safeParse({ + ...DEFAULT_READER_SETTINGS, + ...JSON.parse(raw) + }); + return parsed.success ? parsed.data : DEFAULT_READER_SETTINGS; + } catch { + return DEFAULT_READER_SETTINGS; + } +} + +export function saveReaderSettings(settings: ReaderSettings) { + try { + localStorage.setItem(READER_SETTINGS_STORAGE_KEY, JSON.stringify(settings)); + } catch (error) { + // A full quota must not stop anyone reading. + console.warn('Could not save reader settings', error); + } +} + +/** + * The half of the settings that foliate takes as renderer attributes. + * + * The renderer has no JS property API — these must be set with setAttribute. + * Note there is no `margin` shorthand upstream, and no `spread`: on a reflowable + * book a two-page spread is max-column-count 2. + */ +export function toRendererAttributes(s: ReaderSettings): Record { + return { + flow: s.flow, + gap: `${s.gap}%`, + 'margin-top': `${s.margin}px`, + 'margin-bottom': `${s.margin}px`, + 'margin-left': `${s.margin}px`, + 'margin-right': `${s.margin}px`, + 'max-inline-size': `${s.maxInlineSize}px`, + 'max-column-count': String(s.maxColumnCount) + }; +} diff --git a/frontend/src/lib/reader/stylesheet.ts b/frontend/src/lib/reader/stylesheet.ts new file mode 100644 index 0000000..067360e --- /dev/null +++ b/frontend/src/lib/reader/stylesheet.ts @@ -0,0 +1,136 @@ +import type { ReaderSettings } from '$lib/schema/reader'; + +export interface ReaderPalette { + bg: string; + fg: string; + muted: string; + link: string; + dark: boolean; +} + +/** + * Reads the app palette out of the live document. + * + * The reader route uses a +layout@ breakout, so it never runs (root)/+layout and + * cannot reach the theme context. It does not need to: hooks.server.ts inlines + *