From db337715756bb74c995e3b073084b9c56aad4e74 Mon Sep 17 00:00:00 2001 From: John O'Keefe Date: Sun, 19 Apr 2026 21:15:10 -0400 Subject: [PATCH] fix(reader): inject @font-face rules into shadow DOM iframe for font switching The paginator renders book content inside a sandboxed iframe within a closed shadow DOM. @font-face declarations from the parent page's reader-fonts.css are NOT available inside the iframe's document context. All font-family rules fell back to the generic 'serif' system font, making every reading font look identical. Fix: prepend all @font-face declarations (Literata, Crimson Pro, Source Serif 4, EB Garamond, Libertinus Serif, Noto Serif, Charis SIL, IBM Plex Serif) into the CSS string returned by getCSS(), so they're injected into the iframe via renderer.setStyles(). --- web/src/reader/reader.ts | 136 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 136 insertions(+) diff --git a/web/src/reader/reader.ts b/web/src/reader/reader.ts index a199fdb..2acf8e6 100644 --- a/web/src/reader/reader.ts +++ b/web/src/reader/reader.ts @@ -14,6 +14,141 @@ const FONT_MAP: Record = { "ibm-plex": '"IBM Plex Serif"', }; +const FONT_FACE_RULES = ` +@font-face { + font-family: "Literata"; + src: url("/static/fonts/literata/Literata-Variable.woff2") format("woff2"); + font-weight: 100 900; + font-style: normal; +} +@font-face { + font-family: "Literata"; + src: url("/static/fonts/literata/Literata-Italic-Variable.woff2") format("woff2"); + font-weight: 100 900; + font-style: italic; +} +@font-face { + font-family: "Crimson Pro"; + src: url("/static/fonts/crimson/CrimsonPro-Variable.woff2") format("woff2"); + font-weight: 100 900; + font-style: normal; +} +@font-face { + font-family: "Crimson Pro"; + src: url("/static/fonts/crimson/CrimsonPro-Italic-Variable.woff2") format("woff2"); + font-weight: 100 900; + font-style: italic; +} +@font-face { + font-family: "Source Serif 4"; + src: url("/static/fonts/source-serif/SourceSerif4-Variable.woff2") format("woff2"); + font-weight: 100 900; + font-style: normal; +} +@font-face { + font-family: "Source Serif 4"; + src: url("/static/fonts/source-serif/SourceSerif4-Italic-Variable.woff2") format("woff2"); + font-weight: 100 900; + font-style: italic; +} +@font-face { + font-family: "EB Garamond"; + src: url("/static/fonts/eb-garamond/EBGaramond-Variable.woff2") format("woff2"); + font-weight: 100 900; + font-style: normal; +} +@font-face { + font-family: "EB Garamond"; + src: url("/static/fonts/eb-garamond/EBGaramond-Italic-Variable.woff2") format("woff2"); + font-weight: 100 900; + font-style: italic; +} +@font-face { + font-family: "Libertinus Serif"; + src: url("/static/fonts/libertinus/LibertinusSerif-Regular.woff2") format("woff2"); + font-weight: 400; + font-style: normal; +} +@font-face { + font-family: "Libertinus Serif"; + src: url("/static/fonts/libertinus/LibertinusSerif-Bold.woff2") format("woff2"); + font-weight: 700; + font-style: normal; +} +@font-face { + font-family: "Libertinus Serif"; + src: url("/static/fonts/libertinus/LibertinusSerif-Italic.woff2") format("woff2"); + font-weight: 400; + font-style: italic; +} +@font-face { + font-family: "Libertinus Serif"; + src: url("/static/fonts/libertinus/LibertinusSerif-BoldItalic.woff2") format("woff2"); + font-weight: 700; + font-style: italic; +} +@font-face { + font-family: "Noto Serif"; + src: url("/static/fonts/noto-serif/NotoSerif-Variable.woff2") format("woff2"); + font-weight: 100 900; + font-style: normal; +} +@font-face { + font-family: "Noto Serif"; + src: url("/static/fonts/noto-serif/NotoSerif-Italic-Variable.woff2") format("woff2"); + font-weight: 100 900; + font-style: italic; +} +@font-face { + font-family: "Charis SIL"; + src: url("/static/fonts/charis-sil/CharisSIL-Regular.woff2") format("woff2"); + font-weight: 400; + font-style: normal; +} +@font-face { + font-family: "Charis SIL"; + src: url("/static/fonts/charis-sil/CharisSIL-Bold.woff2") format("woff2"); + font-weight: 700; + font-style: normal; +} +@font-face { + font-family: "Charis SIL"; + src: url("/static/fonts/charis-sil/CharisSIL-Italic.woff2") format("woff2"); + font-weight: 400; + font-style: italic; +} +@font-face { + font-family: "Charis SIL"; + src: url("/static/fonts/charis-sil/CharisSIL-BoldItalic.woff2") format("woff2"); + font-weight: 700; + font-style: italic; +} +@font-face { + font-family: "IBM Plex Serif"; + src: url("/static/fonts/ibm-plex/IBMPlexSerif-Regular.woff2") format("woff2"); + font-weight: 400; + font-style: normal; +} +@font-face { + font-family: "IBM Plex Serif"; + src: url("/static/fonts/ibm-plex/IBMPlexSerif-Bold.woff2") format("woff2"); + font-weight: 700; + font-style: normal; +} +@font-face { + font-family: "IBM Plex Serif"; + src: url("/static/fonts/ibm-plex/IBMPlexSerif-Italic.woff2") format("woff2"); + font-weight: 400; + font-style: italic; +} +@font-face { + font-family: "IBM Plex Serif"; + src: url("/static/fonts/ibm-plex/IBMPlexSerif-BoldItalic.woff2") format("woff2"); + font-weight: 700; + font-style: italic; +} +`; + interface ThemeColors { fg: string; bg: string; @@ -124,6 +259,7 @@ const getCSS = ({ }` : ""; return ` + ${FONT_FACE_RULES} @namespace epub "http://www.idpf.org/2007/ops"; html { color-scheme: light dark;