fix(reader): scope reading theme to viewport, wire up font/size/line-height to shadow DOM

Two root causes fixed:

1. Reading theme CSS variables were on document.body, leaking font/color
   into chrome UI. Now scoped to #reader-viewport so chrome keeps its own
   theme (system font, --text-primary colors) while the reading area uses
   reading theme colors/background.

2. getCSS() never received font family, font size, or line height settings.
   The settings UI (dropdowns, sliders) saved values but they were never
   injected into the book's shadow DOM. Now getCSS() accepts all four
   settings and generates proper CSS rules for them.

Changes:
- Wrap foliate-view in #reader-viewport div (absolute positioned between
  chrome bars)
- getCSS() reads computed style from #reader-viewport, not document.body
- getCSS() params expanded: fontFamily, fontSize, lineHeight, justify,
  hyphenate (removed unused 'spacing')
- Added FONT_MAP to translate setting keys to CSS font-family values
- applyTheme() targets #reader-viewport instead of document.body
- Removed dead #reader-viewport typography rules from foliate-themes.css
  (shadow DOM doesn't inherit outer styles), kept only background-color
This commit is contained in:
2026-04-19 20:36:18 -04:00
parent 96effde421
commit 863c0fd9af
4 changed files with 73 additions and 31 deletions
+47 -23
View File
@@ -2,27 +2,48 @@ import "foliate-js/view.js";
import { Alpine } from "../alpine";
import { loadSettings, saveSettings } from "./settings-manager";
import { getToken } from "../storage";
const FONT_MAP: Record<string, string> = {
literata: '"Literata"',
crimson: '"Crimson Pro"',
"source-serif": '"Source Serif 4"',
"eb-garamond": '"EB Garamond"',
libertinus: '"Libertinus Serif"',
"noto-serif": '"Noto Serif"',
"charis-sil": '"Charis SIL"',
"ibm-plex": '"IBM Plex Serif"',
};
const getCSS = ({
spacing,
fontFamily,
fontSize,
lineHeight,
justify,
hyphenate,
}: {
spacing: number;
fontFamily: string;
fontSize: number;
lineHeight: number;
justify: boolean;
hyphenate: boolean;
}) => {
const style = getComputedStyle(document.body);
const el = document.getElementById("reader-viewport");
const style = getComputedStyle(el ?? document.body);
const bg = style.getPropertyValue("--reader-bg").trim() || "#fafafa";
const text = style.getPropertyValue("--reader-text").trim() || "#1a1a1a";
const link = style.getPropertyValue("--reader-link").trim() || "#0066cc";
const selection =
style.getPropertyValue("--reader-selection").trim() || "#b3d9ff";
const font = FONT_MAP[fontFamily] ?? '"Literata"';
return `
@namespace epub "http://www.idpf.org/2007/ops";
html {
color-scheme: light dark;
background-color: ${bg};
color: ${text};
font-family: ${font}, serif;
font-size: ${fontSize}px;
line-height: ${lineHeight};
}
::selection {
background-color: ${selection};
@@ -36,7 +57,7 @@ const getCSS = ({
}
}
p, li, blockquote, dd {
line-height: ${spacing};
line-height: ${lineHeight};
text-align: ${justify ? "justify" : "start"};
-webkit-hyphens: ${hyphenate ? "auto" : "manual"};
hyphens: ${hyphenate ? "auto" : "manual"};
@@ -94,11 +115,8 @@ document.addEventListener("alpine:init", () => {
progressText: "",
sliderValue: 0,
settings: null as ReaderSettings | null,
style: {
spacing: 1.4,
justify: true,
hyphenate: true,
},
justify: true,
hyphenate: true,
tocOpen: false,
settingsOpen: false,
bookmarksOpen: false,
@@ -122,17 +140,16 @@ document.addEventListener("alpine:init", () => {
this.readingFont = this.settings.reading_font || "literata";
this.fontSize = this.settings.font_size || 16;
this.lineHeight = this.settings.line_height || 1.6;
if (this.settings.reading_theme) {
document.body.classList.add(`theme-${this.settings.reading_theme}`);
}
if (this.settings.reading_mode) {
this.readingMode = this.settings.reading_mode;
} else {
this.readingMode = this.detectChromeDarkMode() ? "dark" : "light";
}
if (this.readingMode === "dark") {
document.body.classList.add("dark");
}
}
const viewport = document.getElementById("reader-viewport")!;
viewport.classList.add(`theme-${this.readingTheme}`);
if (this.readingMode === "dark") {
viewport.classList.add("dark");
}
this.view = document.getElementById("reader-view") as any;
const resp = await fetch(config.fileUrl, {
@@ -151,7 +168,7 @@ document.addEventListener("alpine:init", () => {
this.zoomPercent = this.renderer.zoomPercent;
});
} else {
this.renderer.setStyles?.(getCSS(this.style));
this.renderer.setStyles?.(this.buildCSS());
}
this.view.addEventListener("load", (e: any) => {
const { doc } = e.detail;
@@ -285,13 +302,11 @@ document.addEventListener("alpine:init", () => {
}
},
applyTheme() {
document.body.className = document.body.className
.replace(/theme-(?!tokyo|dracula|nord|solarized|monokai|one-dark|material|catppuccin)[\w-]+/g, "")
.replace(/\bdark\b/g, "")
.trim();
document.body.classList.add(`theme-${this.readingTheme}`);
const viewport = document.getElementById("reader-viewport")!;
viewport.className = "absolute inset-x-0 top-[52px] bottom-[52px]";
viewport.classList.add(`theme-${this.readingTheme}`);
if (this.readingMode === "dark") {
document.body.classList.add("dark");
viewport.classList.add("dark");
}
this.applyStyles();
saveSettings({
@@ -321,7 +336,16 @@ document.addEventListener("alpine:init", () => {
},
applyStyles() {
if (!this.renderer?.setStyles) return;
this.renderer.setStyles(getCSS(this.style));
this.renderer.setStyles(this.buildCSS());
},
buildCSS() {
return getCSS({
fontFamily: this.readingFont,
fontSize: this.fontSize,
lineHeight: this.lineHeight,
justify: this.justify,
hyphenate: this.hyphenate,
});
},
async addBookmark() {
const token = getToken();