feat(fonts): apply per-book body and heading fonts on screen

Split heading typography onto a new --font-heading token and set both
--font-book and --font-heading from the active book's fonts when it
opens or its setup changes. Declare @font-face for the new bundled
fonts.
This commit is contained in:
pj committed 2026-06-22 14:19:01 -04:00
1 parent 124c1947c1
commit 28367b6670
5 files changed
+101 -6

No files matched your search

+13
View File
@@ -0,0 +1,13 @@
import { type BookFonts, fontStack } from "./model/fonts";
export function applyBookFonts(fonts: BookFonts): void {
const root = document.documentElement;
root.style.setProperty("--font-book", fontStack(fonts.body));
root.style.setProperty("--font-heading", fontStack(fonts.heading));
}
export function resetBookFonts(): void {
const root = document.documentElement;
root.style.removeProperty("--font-book");
root.style.removeProperty("--font-heading");
}
+9 -1
View File
@@ -14,6 +14,7 @@ import {
} from "../model/book"; } from "../model/book";
import { loadActiveChapter, saveActiveChapter } from "../editor/positions"; import { loadActiveChapter, saveActiveChapter } from "../editor/positions";
import { saveBook } from "../library"; import { saveBook } from "../library";
import { applyBookFonts, resetBookFonts } from "../book-style";
export const COVER_ID = "__cover__"; export const COVER_ID = "__cover__";
@@ -60,10 +61,12 @@ export const useBook = create<BookState>((set, get) => ({
const saved = loadActiveChapter(normalized.id); const saved = loadActiveChapter(normalized.id);
const valid = saved === COVER_ID || normalized.chapters.some((c) => c.id === saved); const valid = saved === COVER_ID || normalized.chapters.some((c) => c.id === saved);
const activeChapterId = valid ? (saved as string) : normalized.chapters[0]?.id ?? ""; const activeChapterId = valid ? (saved as string) : normalized.chapters[0]?.id ?? "";
applyBookFonts(normalized.settings.fonts);
set({ book: normalized, activeChapterId, dirty: false }); set({ book: normalized, activeChapterId, dirty: false });
}, },
closeBook: () => { closeBook: () => {
flushOutgoing(get().book, get().dirty); flushOutgoing(get().book, get().dirty);
resetBookFonts();
set({ book: null, activeChapterId: "", dirty: false }); set({ book: null, activeChapterId: "", dirty: false });
}, },
setActiveChapter: (id) => set({ activeChapterId: id }), setActiveChapter: (id) => set({ activeChapterId: id }),
@@ -140,7 +143,12 @@ export const useBook = create<BookState>((set, get) => ({
setMetadata: (patch) => setMetadata: (patch) =>
set((s) => (s.book ? { dirty: true, book: { ...s.book, metadata: { ...s.book.metadata, ...patch } } } : {})), set((s) => (s.book ? { dirty: true, book: { ...s.book, metadata: { ...s.book.metadata, ...patch } } } : {})),
setSettings: (patch) => setSettings: (patch) =>
set((s) => (s.book ? { dirty: true, book: { ...s.book, settings: { ...s.book.settings, ...patch } } } : {})), set((s) => {
if (!s.book) return {};
const book = { ...s.book, settings: { ...s.book.settings, ...patch } };
if (patch.fonts) applyBookFonts(book.settings.fonts);
return { dirty: true, book };
}),
setCover: (patch) => setCover: (patch) =>
set((s) => (s.book ? { dirty: true, book: { ...s.book, cover: { ...s.book.cover, ...patch } } } : {})), set((s) => (s.book ? { dirty: true, book: { ...s.book, cover: { ...s.book.cover, ...patch } } } : {})),
markSaved: () => set({ dirty: false }), markSaved: () => set({ dirty: false }),
+14 -5
View File
@@ -427,6 +427,7 @@ body.resizing {
} }
.chapter-title { .chapter-title {
font-family: var(--font-heading);
font-weight: 500; font-weight: 500;
font-size: 31px; font-size: 31px;
line-height: 1.16; line-height: 1.16;
@@ -434,6 +435,10 @@ body.resizing {
margin: 15px 0 0; margin: 15px 0 0;
} }
.prose :is(h1, h2, h3) {
font-family: var(--font-heading);
}
.prose p { .prose p {
font-size: 19px; font-size: 19px;
line-height: 1.68; line-height: 1.68;
@@ -565,11 +570,15 @@ body.resizing {
} }
.page .p-title { .page .p-title {
font-family: var(--font-book); font-family: var(--font-heading);
font-size: 12px; font-size: 12px;
margin-top: 5px; margin-top: 5px;
} }
.page :is(h2, h3) {
font-family: var(--font-heading);
}
.page p { .page p {
font-family: var(--font-book); font-family: var(--font-book);
font-size: 6.6px; font-size: 6.6px;
@@ -620,7 +629,7 @@ body.resizing {
border: 0; border: 0;
background: transparent; background: transparent;
text-align: center; text-align: center;
font-family: var(--font-book); font-family: var(--font-heading);
font-weight: 500; font-weight: 500;
font-size: 31px; font-size: 31px;
line-height: 1.16; line-height: 1.16;
@@ -1432,7 +1441,7 @@ body.resizing {
} }
.cover-title { .cover-title {
font-family: var(--font-book); font-family: var(--font-heading);
font-weight: 500; font-weight: 500;
font-size: 9.5cqw; font-size: 9.5cqw;
line-height: 1.08; line-height: 1.08;
@@ -2108,7 +2117,7 @@ body.resizing {
} }
.device-title { .device-title {
font-family: var(--font-book); font-family: var(--font-heading);
font-weight: 500; font-weight: 500;
font-size: calc(24 * var(--dv-scale)); font-size: calc(24 * var(--dv-scale));
line-height: 1.2; line-height: 1.2;
@@ -2138,7 +2147,7 @@ body.resizing {
.device-body h1, .device-body h1,
.device-body h2, .device-body h2,
.device-body h3 { .device-body h3 {
font-family: var(--font-book); font-family: var(--font-heading);
font-weight: 600; font-weight: 600;
line-height: 1.25; line-height: 1.25;
margin: 1.2em 0 0.5em; margin: 1.2em 0 0.5em;
+64
View File
@@ -29,3 +29,67 @@
font-style: italic; font-style: italic;
font-display: block; font-display: block;
} }
@font-face {
font-family: "EB Garamond";
src: url("/fonts/EBGaramond-VF.ttf") format("truetype-variations");
font-weight: 400 800;
font-style: normal;
font-display: block;
}
@font-face {
font-family: "EB Garamond";
src: url("/fonts/EBGaramond-Italic-VF.ttf") format("truetype-variations");
font-weight: 400 800;
font-style: italic;
font-display: block;
}
@font-face {
font-family: "Lora";
src: url("/fonts/Lora-VF.ttf") format("truetype-variations");
font-weight: 400 700;
font-style: normal;
font-display: block;
}
@font-face {
font-family: "Lora";
src: url("/fonts/Lora-Italic-VF.ttf") format("truetype-variations");
font-weight: 400 700;
font-style: italic;
font-display: block;
}
@font-face {
font-family: "Source Serif 4";
src: url("/fonts/SourceSerif4-VF.ttf") format("truetype-variations");
font-weight: 200 900;
font-style: normal;
font-display: block;
}
@font-face {
font-family: "Source Serif 4";
src: url("/fonts/SourceSerif4-Italic-VF.ttf") format("truetype-variations");
font-weight: 200 900;
font-style: italic;
font-display: block;
}
@font-face {
font-family: "Fraunces";
src: url("/fonts/Fraunces-VF.ttf") format("truetype-variations");
font-weight: 100 900;
font-style: normal;
font-display: block;
}
@font-face {
font-family: "Fraunces";
src: url("/fonts/Fraunces-Italic-VF.ttf") format("truetype-variations");
font-weight: 100 900;
font-style: italic;
font-display: block;
}
+1
View File
@@ -1,6 +1,7 @@
:root { :root {
--font-ui: "Hanken Grotesk", ui-sans-serif, system-ui, -apple-system, sans-serif; --font-ui: "Hanken Grotesk", ui-sans-serif, system-ui, -apple-system, sans-serif;
--font-book: "Literata", Georgia, "Times New Roman", serif; --font-book: "Literata", Georgia, "Times New Roman", serif;
--font-heading: "Literata", Georgia, "Times New Roman", serif;
--r-sm: 5px; --r-sm: 5px;
--r-md: 8px; --r-md: 8px;