/** * The curated self-hosted font catalog (ADR 0016). One maintained list drives * everything: the build step downloads these families' WOFF2 subsets into the * web image, the pond-settings Appearance UI offers them, and the attribution * page lists their licenses. Adding a font is a change here + an image rebuild * — there is no runtime font management (deliberately small surface). * * Fonts are served only from the instance itself (`font-src 'self'`); a * visitor's browser makes zero third-party requests (the GDPR guarantee, * security.md). */ export type FontCategory = 'sans-serif' | 'serif' | 'monospace'; export type FontLicense = 'OFL-1.1' | 'Apache-2.0'; export interface FontCatalogEntry { /** google-webfonts-helper id — the download key for the build step. */ id: string; /** CSS `font-family` name. */ family: string; category: FontCategory; /** Weights downloaded and offered (a subset of what upstream provides). */ weights: number[]; license: FontLicense; licenseUrl: string; } /** Every catalog entry MUST carry license info — the build step fails otherwise * (ADR 0016). Roboto ships under Apache-2.0; the rest under the SIL OFL 1.1. */ export const FONT_CATALOG: readonly FontCatalogEntry[] = [ { id: 'roboto', family: 'Roboto', category: 'sans-serif', weights: [200, 300, 400, 500, 700], license: 'Apache-2.0', licenseUrl: 'https://www.apache.org/licenses/LICENSE-2.0', }, { id: 'open-sans', family: 'Open Sans', category: 'sans-serif', weights: [300, 400, 600, 700], license: 'OFL-1.1', licenseUrl: 'https://openfontlicense.org', }, { id: 'lato', family: 'Lato', category: 'sans-serif', weights: [300, 400, 700], license: 'OFL-1.1', licenseUrl: 'https://openfontlicense.org', }, { id: 'montserrat', family: 'Montserrat', category: 'sans-serif', weights: [300, 400, 500, 700], license: 'OFL-1.1', licenseUrl: 'https://openfontlicense.org', }, { id: 'inter', family: 'Inter', category: 'sans-serif', weights: [300, 400, 500, 700], license: 'OFL-1.1', licenseUrl: 'https://openfontlicense.org', }, { id: 'nunito', family: 'Nunito', category: 'sans-serif', weights: [300, 400, 600, 700], license: 'OFL-1.1', licenseUrl: 'https://openfontlicense.org', }, { id: 'pt-sans', family: 'PT Sans', category: 'sans-serif', weights: [400, 700], license: 'OFL-1.1', licenseUrl: 'https://openfontlicense.org', }, { id: 'merriweather', family: 'Merriweather', category: 'serif', weights: [300, 400, 700], license: 'OFL-1.1', licenseUrl: 'https://openfontlicense.org', }, { id: 'lora', family: 'Lora', category: 'serif', weights: [400, 500, 700], license: 'OFL-1.1', licenseUrl: 'https://openfontlicense.org', }, { id: 'source-serif-4', family: 'Source Serif 4', category: 'serif', weights: [300, 400, 600, 700], license: 'OFL-1.1', licenseUrl: 'https://openfontlicense.org', }, { id: 'playfair-display', family: 'Playfair Display', category: 'serif', weights: [400, 500, 700], license: 'OFL-1.1', licenseUrl: 'https://openfontlicense.org', }, { id: 'fira-code', family: 'Fira Code', category: 'monospace', weights: [300, 400, 500, 700], license: 'OFL-1.1', licenseUrl: 'https://openfontlicense.org', }, { id: 'ibm-plex-mono', family: 'IBM Plex Mono', category: 'monospace', weights: [300, 400, 500, 700], license: 'OFL-1.1', licenseUrl: 'https://openfontlicense.org', }, { id: 'jetbrains-mono', family: 'JetBrains Mono', category: 'monospace', weights: [400, 500, 700], license: 'OFL-1.1', licenseUrl: 'https://openfontlicense.org', }, { id: 'source-code-pro', family: 'Source Code Pro', category: 'monospace', weights: [400, 500, 700], license: 'OFL-1.1', licenseUrl: 'https://openfontlicense.org', }, ]; /** Per-vision defaults (ADR 0016) — a pond with no saved fonts renders these. * Kept in step with `pondSettingsSchema`'s `fonts` defaults. */ export const DEFAULT_FONTS = { heading: { family: 'Roboto', weight: 400 }, body: { family: 'Roboto', weight: 200 }, mono: { family: 'Fira Code', weight: 400 }, } as const; /** System fallback per category — used while a WOFF2 loads and if it is absent. */ export const FONT_FALLBACKS: Readonly> = { 'sans-serif': "system-ui, -apple-system, 'Segoe UI', sans-serif", serif: "Georgia, 'Times New Roman', serif", monospace: "ui-monospace, 'SFMono-Regular', Menlo, monospace", }; /** URL/file-safe slug for a family (matches the on-disk `/fonts//` layout). */ export function fontSlug(family: string): string { return family .toLowerCase() .replace(/[^a-z0-9]+/g, '-') .replace(/(^-|-$)/g, ''); } export function fontEntry(family: string): FontCatalogEntry | undefined { return FONT_CATALOG.find((entry) => entry.family === family); } /** * The `font-family` stack for a chosen family: the family itself (when it is in * the catalog) ahead of the category's system fallback, so text stays readable * before the WOFF2 loads or if the catalog font is unknown. */ export function fontStack(family: string): string { const entry = fontEntry(family); const fallback = FONT_FALLBACKS[entry?.category ?? 'sans-serif']; return entry ? `'${family}', ${fallback}` : fallback; }