a tool for shared writing and social publishing

implemnt basic font logic

+197 -16
+2 -1
app/[leaflet_id]/Leaflet.tsx
··· 18 18 token: PermissionToken; 19 19 initialFacts: Fact<Attribute>[]; 20 20 leaflet_id: string; 21 + initialFontId?: string; 21 22 }) { 22 23 return ( 23 24 <ReplicacheProvider ··· 29 30 <EntitySetProvider 30 31 set={props.token.permission_token_rights[0].entity_set} 31 32 > 32 - <ThemeProvider entityID={props.leaflet_id}> 33 + <ThemeProvider entityID={props.leaflet_id} initialFontId={props.initialFontId}> 33 34 <ThemeBackgroundProvider entityID={props.leaflet_id}> 34 35 <UpdateLeafletTitle entityID={props.leaflet_id} /> 35 36 <AddLeafletToHomepage />
+22 -12
app/[leaflet_id]/page.tsx
··· 14 14 import { get_leaflet_data } from "app/api/rpc/[command]/get_leaflet_data"; 15 15 import { NotFoundLayout } from "components/PageLayouts/NotFoundLayout"; 16 16 import { getPublicationMetadataFromLeafletData } from "src/utils/getPublicationMetadataFromLeafletData"; 17 + import { FontLoader, extractFontFromFacts } from "components/FontLoader"; 17 18 18 19 export const preferredRegion = ["sfo1"]; 19 20 export const dynamic = "force-dynamic"; ··· 48 49 getPollData(res.data.permission_token_rights.map((ptr) => ptr.entity_set)), 49 50 ]); 50 51 let initialFacts = (data as unknown as Fact<Attribute>[]) || []; 52 + 53 + // Extract font setting from facts for server-side font loading 54 + const fontId = extractFontFromFacts(initialFacts as any, rootEntity); 55 + 51 56 return ( 52 - <PageSWRDataProvider 53 - rsvp_data={rsvp_data} 54 - poll_data={poll_data} 55 - leaflet_id={res.data.id} 56 - leaflet_data={res} 57 - > 58 - <Leaflet 59 - initialFacts={initialFacts} 60 - leaflet_id={rootEntity} 61 - token={res.data} 62 - /> 63 - </PageSWRDataProvider> 57 + <> 58 + {/* Server-side font loading with preload and @font-face */} 59 + <FontLoader fontId={fontId} /> 60 + <PageSWRDataProvider 61 + rsvp_data={rsvp_data} 62 + poll_data={poll_data} 63 + leaflet_id={res.data.id} 64 + leaflet_data={res} 65 + > 66 + <Leaflet 67 + initialFacts={initialFacts} 68 + leaflet_id={rootEntity} 69 + token={res.data} 70 + initialFontId={fontId} 71 + /> 72 + </PageSWRDataProvider> 73 + </> 64 74 ); 65 75 } 66 76
+2 -2
app/globals.css
··· 62 62 --shadow-md: 1.2px 2.5px 2.7px -1.8px rgba(var(--primary), 0.1), 63 63 5.6px 11.6px 12.5px -3.5px rgba(var(--primary), 0.15); 64 64 65 - --font-sans: var(--font-quattro); 65 + --font-sans: var(--theme-font, var(--font-quattro)); 66 66 --font-serif: Garamond; 67 67 } 68 68 ··· 194 194 } 195 195 196 196 pre { 197 - font-family: var(--font-quattro); 197 + font-family: var(--theme-font, --font-quattro); 198 198 } 199 199 200 200 p {
+55
components/FontLoader.tsx
··· 1 + // Server-side font loading component 2 + // Replicates next/font behavior: preload links, @font-face, and CSS variable 3 + // This is rendered on the server and the link/style tags are hoisted to <head> 4 + 5 + import { getFontConfig, generateFontFaceCSS, getFontPreloadLinks, FontConfig } from "src/fonts"; 6 + 7 + type FontLoaderProps = { 8 + fontId: string | undefined; 9 + }; 10 + 11 + export function FontLoader({ fontId }: FontLoaderProps) { 12 + const font = getFontConfig(fontId); 13 + const preloadLinks = getFontPreloadLinks(font); 14 + const fontFaceCSS = generateFontFaceCSS(font); 15 + 16 + // Generate CSS that sets the font family via CSS variable 17 + const fontVariableCSS = ` 18 + :root { 19 + --theme-font: '${font.fontFamily}', ${font.fallback.join(", ")}; 20 + } 21 + `.trim(); 22 + 23 + return ( 24 + <> 25 + {/* Preload font files - these get hoisted to <head> by React/Next.js */} 26 + {preloadLinks.map((link) => ( 27 + <link 28 + key={link.href} 29 + rel="preload" 30 + href={link.href} 31 + as="font" 32 + type={link.type} 33 + crossOrigin="anonymous" 34 + /> 35 + ))} 36 + {/* @font-face declarations and CSS variable */} 37 + <style 38 + dangerouslySetInnerHTML={{ 39 + __html: `${fontFaceCSS}\n\n${fontVariableCSS}`, 40 + }} 41 + /> 42 + </> 43 + ); 44 + } 45 + 46 + // Helper to extract font from facts array (for server-side use) 47 + export function extractFontFromFacts( 48 + facts: Array<{ entity: string; attribute: string; data: { value: string } }>, 49 + rootEntity: string 50 + ): string | undefined { 51 + const fontFact = facts.find( 52 + (f) => f.entity === rootEntity && f.attribute === "theme/font" 53 + ); 54 + return fontFact?.data?.value; 55 + }
+17
components/ThemeManager/ThemeProvider.tsx
··· 22 22 PublicationThemeProvider, 23 23 } from "./PublicationThemeProvider"; 24 24 import { getColorDifference } from "./themeUtils"; 25 + import { getFontConfig, defaultFontId } from "src/fonts"; 25 26 26 27 // define a function to set an Aria Color to a CSS Variable in RGB 27 28 function setCSSVariableToColor( ··· 38 39 local?: boolean; 39 40 children: React.ReactNode; 40 41 className?: string; 42 + initialFontId?: string; 41 43 }) { 42 44 let { data: pub, normalizedPublication } = useLeafletPublicationData(); 43 45 if (!pub || !pub.publications) return <LeafletThemeProvider {...props} />; ··· 56 58 entityID: string | null; 57 59 local?: boolean; 58 60 children: React.ReactNode; 61 + initialFontId?: string; 59 62 }) { 60 63 let bgLeaflet = useColorAttribute(props.entityID, "theme/page-background"); 61 64 let bgPage = useColorAttribute(props.entityID, "theme/card-background"); ··· 76 79 let accent2 = useColorAttribute(props.entityID, "theme/accent-text"); 77 80 78 81 let pageWidth = useEntity(props.entityID, "theme/page-width"); 82 + // Use initialFontId as fallback until Replicache syncs 83 + let fontId = useEntity(props.entityID, "theme/font")?.data.value ?? props.initialFontId; 79 84 80 85 return ( 81 86 <CardBorderHiddenContext.Provider value={!!cardBorderHiddenValue}> ··· 92 97 showPageBackground={showPageBackground} 93 98 pageWidth={pageWidth?.data.value} 94 99 hasBackgroundImage={hasBackgroundImage} 100 + fontId={fontId} 95 101 > 96 102 {props.children} 97 103 </BaseThemeProvider> ··· 113 119 showPageBackground, 114 120 pageWidth, 115 121 hasBackgroundImage, 122 + fontId, 116 123 children, 117 124 }: { 118 125 local?: boolean; ··· 127 134 highlight2: AriaColor; 128 135 highlight3: AriaColor; 129 136 pageWidth?: number; 137 + fontId?: string; 130 138 children: React.ReactNode; 131 139 }) => { 132 140 // When showPageBackground is false and there's no background image, ··· 167 175 accentContrast = sortedAccents[0]; 168 176 } 169 177 178 + // Get font config for CSS variable 179 + const fontConfig = getFontConfig(fontId); 180 + const themeFontValue = `'${fontConfig.fontFamily}', ${fontConfig.fallback.join(", ")}`; 181 + 170 182 useEffect(() => { 171 183 if (local) return; 172 184 let el = document.querySelector(":root") as HTMLElement; ··· 215 227 "--page-width-setting", 216 228 (pageWidth || 624).toString(), 217 229 ); 230 + 231 + // Set theme font CSS variable 232 + el?.style.setProperty("--theme-font", themeFontValue); 218 233 }, [ 219 234 local, 220 235 bgLeaflet, ··· 227 242 accent2, 228 243 accentContrast, 229 244 pageWidth, 245 + themeFontValue, 230 246 ]); 231 247 return ( 232 248 <div ··· 249 265 "--page-width-setting": pageWidth || 624, 250 266 "--page-width-unitless": pageWidth || 624, 251 267 "--page-width-units": `min(${pageWidth || 624}px, calc(100vw - 12px))`, 268 + "--theme-font": themeFontValue, 252 269 } as CSSProperties 253 270 } 254 271 >
+31
components/ThemeManager/ThemeSetter.tsx
··· 22 22 import { useLeafletPublicationData } from "components/PageSWRDataProvider"; 23 23 import { useIsMobile } from "src/hooks/isMobile"; 24 24 import { Toggle } from "components/Toggle"; 25 + import { fonts, defaultFontId } from "src/fonts"; 25 26 26 27 export type pickers = 27 28 | "null" ··· 184 185 /> 185 186 </div> 186 187 {!props.home && <WatermarkSetter entityID={props.entityID} />} 188 + {!props.home && <FontPicker entityID={props.entityID} />} 187 189 </div> 188 190 ); 189 191 }; 192 + 193 + function FontPicker(props: { entityID: string }) { 194 + let { rep } = useReplicache(); 195 + let currentFont = useEntity(props.entityID, "theme/font"); 196 + 197 + return ( 198 + <div className="flex flex-col gap-1 mt-2"> 199 + <label className="font-bold text-sm">Font</label> 200 + <select 201 + className="input-with-border w-full" 202 + value={currentFont?.data.value || defaultFontId} 203 + onChange={(e) => { 204 + rep?.mutate.assertFact({ 205 + entity: props.entityID, 206 + attribute: "theme/font", 207 + data: { type: "string", value: e.target.value }, 208 + }); 209 + }} 210 + > 211 + {Object.values(fonts).map((font) => ( 212 + <option key={font.id} value={font.id}> 213 + {font.displayName} 214 + </option> 215 + ))} 216 + </select> 217 + </div> 218 + ); 219 + } 220 + 190 221 function WatermarkSetter(props: { entityID: string }) { 191 222 let { rep } = useReplicache(); 192 223 let checked = useEntity(props.entityID, "theme/page-leaflet-watermark");
public/fonts/Lora-Italic-VariableFont.ttf

This is a binary file and will not be displayed.

public/fonts/Lora-VariableFont.ttf

This is a binary file and will not be displayed.

+67
src/fonts.ts
··· 1 + // Font configuration for self-hosted custom fonts 2 + // This replicates what next/font does but allows dynamic selection per-leaflet 3 + 4 + export type FontConfig = { 5 + id: string; 6 + displayName: string; 7 + fontFamily: string; 8 + files: { 9 + path: string; 10 + style: "normal" | "italic"; 11 + weight?: string; 12 + }[]; 13 + fallback: string[]; 14 + }; 15 + 16 + export const fonts: Record<string, FontConfig> = { 17 + quattro: { 18 + id: "quattro", 19 + displayName: "Quattro", 20 + fontFamily: "iA Writer Quattro V", 21 + files: [ 22 + { path: "/fonts/iAWriterQuattroV.ttf", style: "normal" }, 23 + { path: "/fonts/iAWriterQuattroV-Italic.ttf", style: "italic" }, 24 + ], 25 + fallback: ["system-ui", "sans-serif"], 26 + }, 27 + lora: { 28 + id: "lora", 29 + displayName: "Lora", 30 + fontFamily: "Lora", 31 + files: [ 32 + { path: "/fonts/Lora-VariableFont.ttf", style: "normal", weight: "400 700" }, 33 + { path: "/fonts/Lora-Italic-VariableFont.ttf", style: "italic", weight: "400 700" }, 34 + ], 35 + fallback: ["Georgia", "serif"], 36 + }, 37 + }; 38 + 39 + export const defaultFontId = "quattro"; 40 + 41 + export function getFontConfig(fontId: string | undefined): FontConfig { 42 + return fonts[fontId || defaultFontId] || fonts[defaultFontId]; 43 + } 44 + 45 + // Generate @font-face CSS for a font 46 + export function generateFontFaceCSS(font: FontConfig): string { 47 + return font.files 48 + .map( 49 + (file) => ` 50 + @font-face { 51 + font-family: '${font.fontFamily}'; 52 + src: url('${file.path}') format('truetype'); 53 + font-style: ${file.style}; 54 + font-weight: ${file.weight || "normal"}; 55 + font-display: swap; 56 + }`.trim() 57 + ) 58 + .join("\n\n"); 59 + } 60 + 61 + // Generate preload link attributes for a font 62 + export function getFontPreloadLinks(font: FontConfig): { href: string; type: string }[] { 63 + return font.files.map((file) => ({ 64 + href: file.path, 65 + type: "font/ttf", 66 + })); 67 + }
+1 -1
tailwind.config.js
··· 65 65 }, 66 66 67 67 fontFamily: { 68 - sans: ["var(--font-quattro)"], 68 + sans: ["var(--theme-font, var(--font-quattro))"], 69 69 serif: ["Garamond"], 70 70 }, 71 71 },