a tool for shared writing and social publishing
at refactor/page-perf 219 lines 7.1 kB view raw
1// Font configuration for Google Fonts and the default system font 2// Allows dynamic font selection per-leaflet 3 4export type FontConfig = { 5 id: string; 6 displayName: string; 7 fontFamily: string; 8 fallback: string[]; 9 baseSize?: number; // base font size in px for document content 10} & ( 11 | { 12 // Google Fonts loaded via CDN 13 type: "google"; 14 googleFontsFamily: string; // e.g., "Open+Sans:ital,wght@0,400;0,700;1,400;1,700" 15 } 16 | { 17 // System fonts or fonts loaded elsewhere (e.g. next/font/local) 18 type: "system"; 19 } 20); 21 22export const fonts: Record<string, FontConfig> = { 23 quattro: { 24 id: "quattro", 25 displayName: "iA Writer Quattro", 26 fontFamily: "iA Writer Quattro V", 27 baseSize: 16, 28 type: "system", // Loaded via next/font/local in layout.tsx 29 fallback: ["system-ui", "sans-serif"], 30 }, 31 lora: { 32 id: "lora", 33 displayName: "Lora", 34 fontFamily: "Lora", 35 baseSize: 17, 36 type: "google", 37 googleFontsFamily: "Lora:ital,wght@0,400..700;1,400..700", 38 fallback: ["Georgia", "serif"], 39 }, 40 "atkinson-hyperlegible": { 41 id: "atkinson-hyperlegible", 42 displayName: "Atkinson Hyperlegible", 43 fontFamily: "Atkinson Hyperlegible Next", 44 baseSize: 18, 45 type: "google", 46 googleFontsFamily: 47 "Atkinson+Hyperlegible+Next:ital,wght@0,200..800;1,200..800", 48 fallback: ["system-ui", "sans-serif"], 49 }, 50 // Additional Google Fonts - Mono 51 "sometype-mono": { 52 id: "sometype-mono", 53 displayName: "Sometype Mono", 54 fontFamily: "Sometype Mono", 55 baseSize: 17, 56 type: "google", 57 googleFontsFamily: "Sometype+Mono:ital,wght@0,400;0,700;1,400;1,700", 58 fallback: ["monospace"], 59 }, 60 61 // Additional Google Fonts - Sans 62 montserrat: { 63 id: "montserrat", 64 displayName: "Montserrat", 65 fontFamily: "Montserrat", 66 baseSize: 17, 67 type: "google", 68 googleFontsFamily: "Montserrat:ital,wght@0,400;0,700;1,400;1,700", 69 fallback: ["system-ui", "sans-serif"], 70 }, 71 "source-sans": { 72 id: "source-sans", 73 displayName: "Source Sans 3", 74 fontFamily: "Source Sans 3", 75 baseSize: 18, 76 type: "google", 77 googleFontsFamily: "Source+Sans+3:ital,wght@0,400;0,700;1,400;1,700", 78 fallback: ["system-ui", "sans-serif"], 79 }, 80}; 81 82export const defaultFontId = "quattro"; 83export const defaultBaseSize = 16; 84 85// Parse a Google Fonts URL or string to extract the font name and family parameter 86// Supports various formats: 87// - Full URL: https://fonts.googleapis.com/css2?family=Open+Sans:wght@400;700&display=swap 88// - Family param: Open+Sans:ital,wght@0,400;0,700 89// - Just font name: Open Sans 90export function parseGoogleFontInput(input: string): { 91 fontName: string; 92 googleFontsFamily: string; 93} | null { 94 const trimmed = input.trim(); 95 if (!trimmed) return null; 96 97 // Try to parse as full URL 98 try { 99 const url = new URL(trimmed); 100 const family = url.searchParams.get("family"); 101 if (family) { 102 // Extract font name from family param (before the colon if present) 103 const fontName = family.split(":")[0].replace(/\+/g, " "); 104 return { fontName, googleFontsFamily: family }; 105 } 106 } catch { 107 // Not a valid URL, continue with other parsing 108 } 109 110 // Check if it's a family parameter with weight/style specifiers (contains : or @) 111 if (trimmed.includes(":") || trimmed.includes("@")) { 112 const fontName = trimmed.split(":")[0].replace(/\+/g, " "); 113 // Ensure plus signs are used for spaces in the family param 114 const googleFontsFamily = trimmed.includes("+") 115 ? trimmed 116 : trimmed.replace(/ /g, "+"); 117 return { fontName, googleFontsFamily }; 118 } 119 120 // Treat as just a font name - construct a basic family param with common weights 121 const fontName = trimmed.replace(/\+/g, " "); 122 const googleFontsFamily = `${trimmed.replace(/ /g, "+")}:ital,wght@0,400;0,700;1,400;1,700`; 123 return { fontName, googleFontsFamily }; 124} 125 126// Custom font ID format: "custom:FontName:googleFontsFamily" 127export function createCustomFontId( 128 fontName: string, 129 googleFontsFamily: string, 130): string { 131 return `custom:${fontName}:${googleFontsFamily}`; 132} 133 134export function isCustomFontId(fontId: string): boolean { 135 return fontId.startsWith("custom:"); 136} 137 138export function parseCustomFontId(fontId: string): { 139 fontName: string; 140 googleFontsFamily: string; 141} | null { 142 if (!isCustomFontId(fontId)) return null; 143 const parts = fontId.slice("custom:".length).split(":"); 144 if (parts.length < 2) return null; 145 const fontName = parts[0]; 146 const googleFontsFamily = parts.slice(1).join(":"); 147 return { fontName, googleFontsFamily }; 148} 149 150// Ensure a Google Fonts family string includes italic variants and standard weights. 151// Handles already-stored custom font IDs that may only specify upright weights. 152function ensureItalicWeights(googleFontsFamily: string): string { 153 const [name, spec] = googleFontsFamily.split(/:(.+)/); 154 if (!name) return googleFontsFamily; 155 156 // Already includes italic axis — leave as-is 157 if (spec && spec.includes("ital")) return googleFontsFamily; 158 159 // Has wght@ with values — add italic variants for each weight 160 if (spec) { 161 const wghtMatch = spec.match(/wght@(.+)/); 162 if (wghtMatch) { 163 const weights = wghtMatch[1]; 164 // Variable range like 400..700 165 if (weights.includes("..")) { 166 return `${name}:ital,wght@0,${weights};1,${weights}`; 167 } 168 // Discrete weights like 400;700 169 const uprightWeights = weights.split(";").map((w) => `0,${w}`); 170 const italicWeights = weights.split(";").map((w) => `1,${w}`); 171 return `${name}:ital,wght@${[...uprightWeights, ...italicWeights].join(";")}`; 172 } 173 } 174 175 // No weight spec at all — add standard weights with italics 176 return `${name}:ital,wght@0,400;0,700;1,400;1,700`; 177} 178 179export function getFontConfig(fontId: string | undefined): FontConfig { 180 if (!fontId) return fonts[defaultFontId]; 181 182 // Check for custom font 183 if (isCustomFontId(fontId)) { 184 const parsed = parseCustomFontId(fontId); 185 if (parsed) { 186 return { 187 id: fontId, 188 displayName: parsed.fontName, 189 fontFamily: parsed.fontName, 190 type: "google", 191 googleFontsFamily: ensureItalicWeights(parsed.googleFontsFamily), 192 fallback: ["system-ui", "sans-serif"], 193 }; 194 } 195 } 196 197 return fonts[fontId] || fonts[defaultFontId]; 198} 199 200// Get Google Fonts URL for a font 201// Using display=swap per Google's recommendation: shows fallback immediately, swaps when ready 202// This is better UX than blocking text rendering (display=block) 203export function getGoogleFontsUrl(font: FontConfig): string | null { 204 if (font.type !== "google") return null; 205 return `https://fonts.googleapis.com/css2?family=${font.googleFontsFamily}&display=swap`; 206} 207 208// Get the base font size for a font config 209export function getFontBaseSize(font: FontConfig): number { 210 return font.baseSize ?? defaultBaseSize; 211} 212 213// Get the CSS font-family value with fallbacks 214export function getFontFamilyValue(font: FontConfig): string { 215 const family = font.fontFamily.includes(" ") 216 ? `'${font.fontFamily}'` 217 : font.fontFamily; 218 return [family, ...font.fallback].join(", "); 219}