Files
chitai/frontend/src/lib/reader/stylesheet.ts
T
patrick 190e7af76d feat: replace the epub reader with foliate-js
Drops epub.js's 1600-location pre-pass and its localStorage cache: foliate
hands back a CFI and an overall fraction on every relocate, so first open no
longer freezes.

Also fixes resuming (pre-resolves the stored CFI, falling back to the stored
percentage rather than silently resetting to page one), progress saving
(missing Content-Type, and no flush on the way out), and arrow keys inside
the book iframe.
2026-08-11 22:21:15 -04:00

139 lines
4.1 KiB
TypeScript

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.
*
* `dark` is passed in rather than read off the class list, because mode-watcher's
* store is the authority and the class trails it by a frame. Call this after that
* frame, though — the token values themselves do come from the class.
*/
export function readReaderPalette(dark: boolean): 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
};
}
/**
* 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];
}