feat: add reader settings state and stylesheet generator
Settings are validated on read, not only on write: several go straight onto foliate's renderer as custom-element attributes, where a bad value wedges the layout silently rather than throwing. Unknown keys are stripped and missing ones filled from the defaults, so settings added later do not invalidate what a reader already has stored. The stylesheet is built as foliate's [before, after] pair. The paginator prepends the first to the section's <head> and appends the second, so the book's own CSS overrides the first and loses to the second. Typography goes in the first — a book that styles its own headings still gets to, and font size lands on <html> without !important so rem/em headings scale rather than being flattened. Colour goes in the second, because most EPUBs set a body background and would otherwise leave the page white against a dark UI. Font choices are reused from the app theme's FONT_STACKS rather than duplicated, so the reader cannot drift from the rest of the UI. Rename the ambient declaration to foliate-js.d.ts: as foliate.d.ts beside foliate.ts, TypeScript takes it for that file's emitted declaration, drops it from the program, and every $foliate import fails with TS2307.
This commit is contained in:
@@ -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
|
||||
* <style id="chitai-theme"> with the full palette into every page, so the tokens
|
||||
* are resolved on documentElement before first paint.
|
||||
*
|
||||
* Call this after the dark class has landed, not in the same tick as a mode flip.
|
||||
*/
|
||||
export function readReaderPalette(): ReaderPalette {
|
||||
const styles = getComputedStyle(document.documentElement);
|
||||
const token = (name: string, fallback: string) =>
|
||||
styles.getPropertyValue(name).trim() || fallback;
|
||||
|
||||
return {
|
||||
// --card rather than --background: the book is a sheet of paper on the page.
|
||||
bg: token('--card', '#ffffff'),
|
||||
fg: token('--foreground', '#000000'),
|
||||
muted: token('--muted-foreground', '#666666'),
|
||||
link: token('--primary', '#0066cc'),
|
||||
dark: document.documentElement.classList.contains('dark')
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the two stylesheets foliate injects into each section document.
|
||||
*
|
||||
* The paginator creates two <style> elements per document: the first is
|
||||
* head.prepend()ed, so the book's own CSS overrides it; the second is
|
||||
* head.append()ed, so it wins on equal specificity. setStyles accepts the pair
|
||||
* as a tuple and re-applies it on every section load.
|
||||
*
|
||||
* The split is the point. Typography goes in `before` so a book that styles its
|
||||
* own headings still gets to; colour goes in `after` because most EPUBs set a
|
||||
* body background and would otherwise leave the page white against a dark UI.
|
||||
*/
|
||||
export function buildReaderStyles(
|
||||
s: ReaderSettings,
|
||||
p: ReaderPalette
|
||||
): [before: string, after: string] {
|
||||
const before = `
|
||||
@namespace epub "http://www.idpf.org/2007/ops";
|
||||
|
||||
/* On html, not body, and without !important: the book's rem/em headings
|
||||
then scale with the setting instead of being flattened to one size. */
|
||||
html {
|
||||
font-size: ${s.fontSize}px;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: ${s.fontFamily};
|
||||
font-weight: ${s.fontWeight};
|
||||
line-height: ${s.lineHeight};
|
||||
letter-spacing: ${s.letterSpacing}em;
|
||||
}
|
||||
|
||||
p, li, blockquote, dd, div {
|
||||
line-height: ${s.lineHeight};
|
||||
text-align: ${s.justify ? 'justify' : 'start'};
|
||||
-webkit-hyphens: ${s.hyphenate ? 'auto' : 'manual'};
|
||||
hyphens: ${s.hyphenate ? 'auto' : 'manual'};
|
||||
-webkit-hyphenate-limit-before: 3;
|
||||
-webkit-hyphenate-limit-after: 2;
|
||||
-webkit-hyphenate-limit-lines: 2;
|
||||
hanging-punctuation: allow-end last;
|
||||
widows: 2;
|
||||
orphans: 2;
|
||||
}
|
||||
|
||||
/* Justification must not silently override an explicit align attribute. */
|
||||
[align="left"] { text-align: left; }
|
||||
[align="right"] { text-align: right; }
|
||||
[align="center"] { text-align: center; }
|
||||
[align="justify"] { text-align: justify; }
|
||||
|
||||
pre {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
`;
|
||||
|
||||
const after = `
|
||||
/* Tells the book's own prefers-color-scheme rules which way we are going.
|
||||
Without it the paginator's media query follows the OS, so a book with
|
||||
dark styles can invert against the app. */
|
||||
html {
|
||||
color-scheme: ${p.dark ? 'dark' : 'light'};
|
||||
}
|
||||
|
||||
/* !important only here. Most EPUBs set their own body background and
|
||||
colour, and an unprioritised override loses to them. */
|
||||
html, body {
|
||||
background: ${p.bg} !important;
|
||||
color: ${p.fg} !important;
|
||||
}
|
||||
|
||||
p, div, span, li, td, th, dl, dd, dt,
|
||||
h1, h2, h3, h4, h5, h6, blockquote, figcaption {
|
||||
color: ${p.fg} !important;
|
||||
}
|
||||
|
||||
a:any-link {
|
||||
color: ${p.link} !important;
|
||||
}
|
||||
|
||||
hr {
|
||||
border-color: ${p.muted} !important;
|
||||
}
|
||||
|
||||
/* Keep line art and diagrams legible on a dark page without touching
|
||||
photographs, which invert badly. */
|
||||
${
|
||||
p.dark
|
||||
? `svg { color: ${p.fg}; }
|
||||
img[src$=".svg"] { filter: invert(1) hue-rotate(180deg); }`
|
||||
: ''
|
||||
}
|
||||
|
||||
img, svg, video {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
`;
|
||||
|
||||
return [before, after];
|
||||
}
|
||||
Reference in New Issue
Block a user