mirror of
https://github.com/priyanshujain/margin.git
synced 2026-10-02 11:07:04 +00:00
feat(fonts): add bundled fonts and per-book font model
Add EB Garamond, Lora, Source Serif 4, and Fraunces (OFL variable fonts) and a font catalog with body/heading pairings. Store the chosen fonts per book in BookSettings.fonts, defaulting to Literata so existing books are unchanged.
This commit is contained in:
1 parent
33ef53936e
commit
124c1947c1
15 files changed
+476
-3
No files matched your search
+2
-1
@@ -2,6 +2,7 @@ import { invoke } from "@tauri-apps/api/core";
|
||||
import { open } from "@tauri-apps/plugin-dialog";
|
||||
import type { JSONContent } from "@tiptap/core";
|
||||
import { type Book, type BookMetadata, type Chapter, type Cover, createCover } from "../model/book";
|
||||
import { DEFAULT_FONTS } from "../model/fonts";
|
||||
|
||||
export interface RawFile {
|
||||
path: string;
|
||||
@@ -515,7 +516,7 @@ export function filesToBook(files: RawFile[], fallbackName = "Imported book"): B
|
||||
id: crypto.randomUUID(),
|
||||
metadata,
|
||||
theme: "quiet-press",
|
||||
settings: { trim: "6x9", bleed: true },
|
||||
settings: { trim: "6x9", bleed: true, fonts: DEFAULT_FONTS },
|
||||
cover: findCover(opf, manifest, byPath),
|
||||
chapters,
|
||||
};
|
||||
|
||||
+4
-2
@@ -1,4 +1,5 @@
|
||||
import type { JSONContent } from "@tiptap/core";
|
||||
import { type BookFonts, DEFAULT_FONTS } from "./fonts";
|
||||
|
||||
export type TrimSize = "6x9" | "5.5x8.5" | "5x8" | "a5";
|
||||
export type FigurePlacement = "inline" | "full-width" | "full-page" | "float-top";
|
||||
@@ -24,6 +25,7 @@ export interface BookMetadata {
|
||||
export interface BookSettings {
|
||||
trim: TrimSize;
|
||||
bleed: boolean;
|
||||
fonts: BookFonts;
|
||||
}
|
||||
|
||||
export type CoverKind = "default" | "image";
|
||||
@@ -149,14 +151,14 @@ export function createCover(): Cover {
|
||||
}
|
||||
|
||||
const DEFAULT_METADATA: BookMetadata = { title: "Untitled", subtitle: "", author: "", isbn: "", language: "en" };
|
||||
const DEFAULT_SETTINGS: BookSettings = { trim: "6x9", bleed: true };
|
||||
const DEFAULT_SETTINGS: BookSettings = { trim: "6x9", bleed: true, fonts: DEFAULT_FONTS };
|
||||
|
||||
export function normalizeBook(book: Book): Book {
|
||||
const chapters = (Array.isArray(book.chapters) ? book.chapters : []).filter(Boolean);
|
||||
return {
|
||||
...book,
|
||||
metadata: { ...DEFAULT_METADATA, ...book.metadata },
|
||||
settings: { ...DEFAULT_SETTINGS, ...book.settings },
|
||||
settings: { ...DEFAULT_SETTINGS, ...book.settings, fonts: { ...DEFAULT_FONTS, ...book.settings?.fonts } },
|
||||
cover: book.cover ? { ...createCover(), ...book.cover } : createCover(),
|
||||
chapters: (chapters.length ? chapters : [createChapter()]).map((c) =>
|
||||
c.updatedAt ? c : { ...c, updatedAt: Date.now() },
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
export type FontCategory = "serif" | "sans" | "display";
|
||||
|
||||
export interface BundledFont {
|
||||
id: string;
|
||||
label: string;
|
||||
family: string;
|
||||
category: FontCategory;
|
||||
regular: string;
|
||||
italic?: string;
|
||||
weight: string;
|
||||
}
|
||||
|
||||
export const BUNDLED_FONTS: BundledFont[] = [
|
||||
{ id: "literata", label: "Literata", family: "Literata", category: "serif", regular: "Literata-VF.ttf", italic: "Literata-Italic-VF.ttf", weight: "200 900" },
|
||||
{ id: "eb-garamond", label: "EB Garamond", family: "EB Garamond", category: "serif", regular: "EBGaramond-VF.ttf", italic: "EBGaramond-Italic-VF.ttf", weight: "400 800" },
|
||||
{ id: "lora", label: "Lora", family: "Lora", category: "serif", regular: "Lora-VF.ttf", italic: "Lora-Italic-VF.ttf", weight: "400 700" },
|
||||
{ id: "source-serif", label: "Source Serif 4", family: "Source Serif 4", category: "serif", regular: "SourceSerif4-VF.ttf", italic: "SourceSerif4-Italic-VF.ttf", weight: "200 900" },
|
||||
{ id: "fraunces", label: "Fraunces", family: "Fraunces", category: "display", regular: "Fraunces-VF.ttf", italic: "Fraunces-Italic-VF.ttf", weight: "100 900" },
|
||||
{ id: "hanken", label: "Hanken Grotesk", family: "Hanken Grotesk", category: "sans", regular: "HankenGrotesk-VF.ttf", italic: "HankenGrotesk-Italic-VF.ttf", weight: "100 900" },
|
||||
];
|
||||
|
||||
export type FontRef =
|
||||
| { kind: "bundled"; id: string }
|
||||
| { kind: "system"; family: string };
|
||||
|
||||
export interface BookFonts {
|
||||
body: FontRef;
|
||||
heading: FontRef;
|
||||
}
|
||||
|
||||
export interface FontPairing {
|
||||
id: string;
|
||||
label: string;
|
||||
body: FontRef;
|
||||
heading: FontRef;
|
||||
}
|
||||
|
||||
const bundled = (id: string): FontRef => ({ kind: "bundled", id });
|
||||
|
||||
export const FONT_PAIRINGS: FontPairing[] = [
|
||||
{ id: "quiet-press", label: "Quiet Press", body: bundled("literata"), heading: bundled("literata") },
|
||||
{ id: "classic", label: "Classic", body: bundled("eb-garamond"), heading: bundled("eb-garamond") },
|
||||
{ id: "editorial", label: "Editorial", body: bundled("source-serif"), heading: bundled("fraunces") },
|
||||
{ id: "modern", label: "Modern", body: bundled("lora"), heading: bundled("hanken") },
|
||||
{ id: "contrast", label: "Contrast", body: bundled("literata"), heading: bundled("hanken") },
|
||||
];
|
||||
|
||||
export const DEFAULT_FONTS: BookFonts = { body: bundled("literata"), heading: bundled("literata") };
|
||||
|
||||
const SERIF_FALLBACK = `Georgia, "Times New Roman", serif`;
|
||||
const SANS_FALLBACK = `ui-sans-serif, system-ui, -apple-system, sans-serif`;
|
||||
|
||||
export function bundledFont(id: string): BundledFont | undefined {
|
||||
return BUNDLED_FONTS.find((f) => f.id === id);
|
||||
}
|
||||
|
||||
export function fontFamilyName(ref: FontRef): string {
|
||||
if (ref.kind === "system") return ref.family;
|
||||
return bundledFont(ref.id)?.family ?? "Literata";
|
||||
}
|
||||
|
||||
export function fontLabel(ref: FontRef): string {
|
||||
if (ref.kind === "system") return ref.family;
|
||||
return bundledFont(ref.id)?.label ?? ref.id;
|
||||
}
|
||||
|
||||
export function fontStack(ref: FontRef): string {
|
||||
if (ref.kind === "bundled") {
|
||||
const font = bundledFont(ref.id);
|
||||
if (!font) return SERIF_FALLBACK;
|
||||
return `"${font.family}", ${font.category === "sans" ? SANS_FALLBACK : SERIF_FALLBACK}`;
|
||||
}
|
||||
return `"${ref.family}", ${SERIF_FALLBACK}`;
|
||||
}
|
||||
|
||||
export function refsEqual(a: FontRef, b: FontRef): boolean {
|
||||
if (a.kind !== b.kind) return false;
|
||||
return a.kind === "bundled" ? a.id === (b as { id: string }).id : a.family === (b as { family: string }).family;
|
||||
}
|
||||
|
||||
export function pairingFor(fonts: BookFonts): string | null {
|
||||
const match = FONT_PAIRINGS.find((p) => refsEqual(p.body, fonts.body) && refsEqual(p.heading, fonts.heading));
|
||||
return match?.id ?? null;
|
||||
}
|
||||
|
||||
export function fontsUsed(fonts: BookFonts): { bundled: BundledFont[]; system: string[] } {
|
||||
const usedBundled = new Map<string, BundledFont>();
|
||||
const usedSystem = new Set<string>();
|
||||
for (const ref of [fonts.body, fonts.heading]) {
|
||||
if (ref.kind === "bundled") {
|
||||
const font = bundledFont(ref.id);
|
||||
if (font) usedBundled.set(font.id, font);
|
||||
} else {
|
||||
usedSystem.add(ref.family);
|
||||
}
|
||||
}
|
||||
return { bundled: [...usedBundled.values()], system: [...usedSystem] };
|
||||
}
|
||||
Reference in new issue
Block a user