Files
chitai/frontend/src/lib/reader/settings.ts
T
patrick 9155129ccd fix: make the reader's typography, width and arrow keys work
Typography was in the stylesheet foliate prepends, which the book overrides;
moved to the appended one. The window keydown handler was never bound.

Also: wider reading area, page on a card, arrows moved onto it, spread gutter,
and the sidebar stays open on chapter clicks.
2026-08-11 22:45:14 -04:00

87 lines
2.8 KiB
TypeScript

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,
// foliate caps the reading area at this times the column count, so 1000 gives
// a 2000px spread — enough to fill a large display rather than leaving the
// book as a narrow strip. Its own default of 720 is noticeably tight.
maxInlineSize: 1000,
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: 400, max: 2400, 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<string, string> {
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)
};
}