From 66f9d44120048c2d2df64b29de562909d1308e97 Mon Sep 17 00:00:00 2001 From: Jared Pereira Date: Fri, 6 Mar 2026 15:51:40 -0500 Subject: [PATCH] Add custom fonts! Squashed commit of the following: commit 8b5625aa7bbdbc89ee3355f4b258ce00690b891f Author: Jared Pereira Date: Fri Mar 6 15:11:02 2026 -0500 remove pub picker on subpage and fix font label commit 4f9a2dc7c38e6abc56a76490ee77d728d5abb452 Author: Jared Pereira Date: Fri Mar 6 15:10:57 2026 -0500 support h4 commit e5d8f977bfdde8a6b97f561c09f320879f4baaa9 Merge: 4d41f6f4 43fa8794 Author: Jared Pereira Date: Fri Mar 6 13:57:36 2026 -0500 Merge branch 'main' into feature/fonts commit 4d41f6f4b4454d565dcece8dc0663ece00e45287 Author: Jared Pereira Date: Fri Mar 6 11:56:35 2026 -0500 merge blockprops style better commit 073e43d90f5bdf532b5192c712cf12f8bb6ee728 Author: Jared Pereira Date: Fri Mar 6 11:12:01 2026 -0500 fix font sizes commit 980fcd7d49beb6625ffd94f62dce7cfa0930b9f0 Author: Jared Pereira Date: Thu Mar 5 22:56:33 2026 -0500 handle header sizes and base size for fonts commit 3e831c1f9ab1f545e5a153300790f29047631e4c Author: Jared Pereira Date: Thu Mar 5 21:53:07 2026 -0500 fix undo manager commit 025ac8d16765272a4fc354e3baf8ad468e922b8d Author: Jared Pereira Date: Thu Mar 5 21:43:46 2026 -0500 fix setting fonts commit a1d30fc91a7b9874f3ef561c855837e5b224af29 Author: Jared Pereira Date: Thu Feb 5 01:01:42 2026 +0000 add fonts commit 83049703cc02efb4c5a4f94ad3dbfb43c50c07a3 Author: Jared Pereira Date: Fri Jan 30 18:09:40 2026 -0500 wip support custom google fonts commit 7235f59982a1a683a1f91da4f7db38dda3f9f73b Author: Jared Pereira Date: Thu Jan 22 17:10:18 2026 -0500 more fonts!! commit 10da7354ddb83a15082b75e317c0b0e3cf971c9b Author: Jared Pereira Date: Thu Jan 22 13:49:15 2026 -0500 implemnt basic font logic --- app/[leaflet_id]/Leaflet.tsx | 8 +- app/[leaflet_id]/page.tsx | 35 ++- app/globals.css | 33 ++- app/layout.tsx | 4 +- .../[rkey]/Blocks/PublishedPageBlock.tsx | 2 +- .../[rkey]/DocumentPageRenderer.tsx | 2 + .../[publication]/[rkey]/PostContent.tsx | 18 +- .../[rkey]/StaticPostContent.tsx | 11 +- app/lish/createPub/updatePublication.ts | 4 + components/Blocks/ExternalLinkBlock.tsx | 2 +- components/Blocks/PageLinkBlock.tsx | 2 +- components/Blocks/TextBlock/index.tsx | 37 ++- components/Blocks/TextBlock/inputRules.ts | 2 +- components/Blocks/TextBlock/useHandlePaste.ts | 5 + components/FontLoader.tsx | 121 +++++++++ components/ThemeManager/PageThemeSetter.tsx | 1 + .../ThemeManager/Pickers/PageThemePickers.tsx | 9 + .../ThemeManager/Pickers/TextPickers.tsx | 214 +++++++++++++++ .../ThemeManager/PubPickers/PubFontPicker.tsx | 179 +++++++++++++ .../PubPickers/PubTextPickers.tsx | 14 - components/ThemeManager/PubThemeSetter.tsx | 17 ++ .../ThemeManager/PublicationThemeProvider.tsx | 5 + components/ThemeManager/ThemeProvider.tsx | 66 +++++ components/ThemeManager/ThemeSetter.tsx | 4 +- lexicons/api/lexicons.ts | 8 + lexicons/api/types/pub/leaflet/publication.ts | 2 + lexicons/pub/leaflet/publication.json | 8 + lexicons/src/publication.ts | 2 + public/fonts/Lora-Italic-Variable.woff2 | Bin 0 -> 40772 bytes public/fonts/Lora-Variable.woff2 | Bin 0 -> 37788 bytes public/fonts/NotoSans-Italic-Variable.woff2 | Bin 0 -> 39520 bytes public/fonts/NotoSans-Variable.woff2 | Bin 0 -> 35820 bytes .../fonts/SourceSans3-Italic-Variable.woff2 | Bin 0 -> 28532 bytes public/fonts/SourceSans3-Variable.woff2 | Bin 0 -> 28740 bytes public/fonts/iAWriterQuattroV-Italic.ttf | Bin 145388 -> 0 bytes public/fonts/iAWriterQuattroV.ttf | Bin 163744 -> 0 bytes public/fonts/iaw-quattro-vf-Italic.woff2 | Bin 0 -> 50104 bytes public/fonts/iaw-quattro-vf.woff2 | Bin 0 -> 51480 bytes src/fonts.ts | 252 ++++++++++++++++++ src/replicache/attributes.ts | 6 +- src/undoManager.ts | 5 +- src/utils/blockTextSize.ts | 7 + tailwind.config.js | 2 +- 43 files changed, 1022 insertions(+), 65 deletions(-) create mode 100644 components/FontLoader.tsx create mode 100644 components/ThemeManager/Pickers/TextPickers.tsx create mode 100644 components/ThemeManager/PubPickers/PubFontPicker.tsx create mode 100644 public/fonts/Lora-Italic-Variable.woff2 create mode 100644 public/fonts/Lora-Variable.woff2 create mode 100644 public/fonts/NotoSans-Italic-Variable.woff2 create mode 100644 public/fonts/NotoSans-Variable.woff2 create mode 100644 public/fonts/SourceSans3-Italic-Variable.woff2 create mode 100644 public/fonts/SourceSans3-Variable.woff2 delete mode 100644 public/fonts/iAWriterQuattroV-Italic.ttf delete mode 100644 public/fonts/iAWriterQuattroV.ttf create mode 100644 public/fonts/iaw-quattro-vf-Italic.woff2 create mode 100644 public/fonts/iaw-quattro-vf.woff2 create mode 100644 src/fonts.ts create mode 100644 src/utils/blockTextSize.ts diff --git a/app/[leaflet_id]/Leaflet.tsx b/app/[leaflet_id]/Leaflet.tsx index a98ee708..7ccc8d92 100644 --- a/app/[leaflet_id]/Leaflet.tsx +++ b/app/[leaflet_id]/Leaflet.tsx @@ -18,6 +18,8 @@ export function Leaflet(props: { token: PermissionToken; initialFacts: Fact[]; leaflet_id: string; + initialHeadingFontId?: string; + initialBodyFontId?: string; }) { return ( - + diff --git a/app/[leaflet_id]/page.tsx b/app/[leaflet_id]/page.tsx index be3c7b31..851ac894 100644 --- a/app/[leaflet_id]/page.tsx +++ b/app/[leaflet_id]/page.tsx @@ -14,6 +14,7 @@ import { supabaseServerClient } from "supabase/serverClient"; import { get_leaflet_data } from "app/api/rpc/[command]/get_leaflet_data"; import { NotFoundLayout } from "components/PageLayouts/NotFoundLayout"; import { getPublicationMetadataFromLeafletData } from "src/utils/getPublicationMetadataFromLeafletData"; +import { FontLoader, extractFontsFromFacts } from "components/FontLoader"; export const preferredRegion = ["sfo1"]; export const dynamic = "force-dynamic"; @@ -48,19 +49,29 @@ export default async function LeafletPage(props: Props) { getPollData(res.data.permission_token_rights.map((ptr) => ptr.entity_set)), ]); let initialFacts = (data as unknown as Fact[]) || []; + + // Extract font settings from facts for server-side font loading + const { headingFontId, bodyFontId } = extractFontsFromFacts(initialFacts as any, rootEntity); + return ( - - - + <> + {/* Server-side font loading with preload and @font-face */} + + + + + ); } diff --git a/app/globals.css b/app/globals.css index 73d26f5b..818a7281 100644 --- a/app/globals.css +++ b/app/globals.css @@ -193,10 +193,6 @@ @apply hover:underline; } - pre { - font-family: var(--font-quattro); - } - p { font-size: inherit; } @@ -205,6 +201,35 @@ @apply text-tertiary; @apply italic; } + + /* Scope custom fonts to document content only (not sidebar/UI chrome) */ + .pageScrollWrapper { + font-family: var(--theme-font, var(--font-quattro)); + font-size: var(--theme-font-base-size, 16px); + } + + .pageScrollWrapper h1, + .pageScrollWrapper h2, + .pageScrollWrapper h3, + .pageScrollWrapper h4 { + font-family: var(--theme-heading-font, var(--theme-font, var(--font-quattro))); + } + + /* Scale heading sizes relative to the custom base font size */ + .pageScrollWrapper h1 { font-size: 2em; } + .pageScrollWrapper h2 { font-size: 1.625em; } + .pageScrollWrapper h3 { font-size: 1.125em; } + .pageScrollWrapper h4 { font-size: 1em; } + + /* Scale text size classes relative to the custom base font size. + Tailwind's text-sm/text-base/text-lg compile to fixed rem values + which ignore the custom base size set on .pageScrollWrapper. */ + .pageScrollWrapper .textSizeSmall { font-size: 0.875em; } + .pageScrollWrapper .textSizeLarge { font-size: 1.125em; } + + .pageScrollWrapper pre { + font-family: var(--theme-font, var(--font-quattro)); + } /*END FONT STYLING*/ /* START GLOBAL STYLING */ diff --git a/app/layout.tsx b/app/layout.tsx index 138eafce..16e89e78 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -39,11 +39,11 @@ export const preferredRegion = ["sfo1"]; const quattro = localFont({ src: [ { - path: "../public/fonts/iAWriterQuattroV.ttf", + path: "../public/fonts/iaw-quattro-vf.woff2", style: "normal", }, { - path: "../public/fonts/iAWriterQuattroV-Italic.ttf", + path: "../public/fonts/iaw-quattro-vf-Italic.woff2", style: "italic", }, ], diff --git a/app/lish/[did]/[publication]/[rkey]/Blocks/PublishedPageBlock.tsx b/app/lish/[did]/[publication]/[rkey]/Blocks/PublishedPageBlock.tsx index dca43492..fbe42825 100644 --- a/app/lish/[did]/[publication]/[rkey]/Blocks/PublishedPageBlock.tsx +++ b/app/lish/[did]/[publication]/[rkey]/Blocks/PublishedPageBlock.tsx @@ -111,7 +111,7 @@ export function DocLinkBlock(props: {
{title && (
+
+

-

+ ); if (b.block.level === 2) return ( -

+

-

+ ); if (b.block.level === 3) return ( -

+

-

+ ); // if (b.block.level === 4) return

{b.block.plaintext}

; // if (b.block.level === 5) return
{b.block.plaintext}
; return ( -
+
+

+

); if (b.block.level === 2) return ( -

+

); if (b.block.level === 3) return ( -

+

); // if (b.block.level === 4) return

{b.block.plaintext}

; // if (b.block.level === 5) return
{b.block.plaintext}
; return ( -
+
); diff --git a/app/lish/createPub/updatePublication.ts b/app/lish/createPub/updatePublication.ts index 77e1a191..4eaf4874 100644 --- a/app/lish/createPub/updatePublication.ts +++ b/app/lish/createPub/updatePublication.ts @@ -319,6 +319,8 @@ export async function updatePublicationTheme({ showPageBackground: boolean; accentBackground: Color; accentText: Color; + headingFont?: string; + bodyFont?: string; }; }): Promise { return withPublicationUpdate( @@ -361,6 +363,8 @@ export async function updatePublicationTheme({ accentText: { ...theme.accentText, }, + headingFont: theme.headingFont, + bodyFont: theme.bodyFont, }; // Derive basicTheme from the theme colors for site.standard.publication diff --git a/components/Blocks/ExternalLinkBlock.tsx b/components/Blocks/ExternalLinkBlock.tsx index 57a92d82..52e32883 100644 --- a/components/Blocks/ExternalLinkBlock.tsx +++ b/components/Blocks/ExternalLinkBlock.tsx @@ -78,7 +78,7 @@ export const ExternalLinkBlock = (
{leafletMetadata[0] && (
{leafletMetadata[0].listData && ( s.editorStates[props.entityID], @@ -262,7 +274,11 @@ export function BaseTextBlock(props: BlockProps & { className?: string }) { // unless we break *only* on urls, this is better than tailwind 'break-all' // b/c break-all can cause breaks in the middle of words, but break-word still // forces break if a single text string (e.g. a url) spans more than a full line - style={{ wordBreak: "break-word" }} + style={{ + wordBreak: "break-word", + fontFamily: props.type === "heading" ? "var(--theme-heading-font)" : "var(--theme-font)", + ...(props.type === "heading" ? { fontSize: headingFontSize[headingLevel?.data.value || 1] } : {}), + }} className={` ${alignmentClass} grow resize-none align-top whitespace-pre-wrap bg-transparent @@ -286,6 +302,7 @@ export function BaseTextBlock(props: BlockProps & { className?: string }) { props.nextBlock === null ? ( // if this is the only block on the page and is empty or is a canvas, show placeholder
{ + new InputRule(/^([#]{1,4})\s$/, (state, match) => { let tr = state.tr; tr.delete(0, match[0].length); let headingLevel = match[1].length; diff --git a/components/Blocks/TextBlock/useHandlePaste.ts b/components/Blocks/TextBlock/useHandlePaste.ts index f3bad456..5a4e021a 100644 --- a/components/Blocks/TextBlock/useHandlePaste.ts +++ b/components/Blocks/TextBlock/useHandlePaste.ts @@ -244,6 +244,11 @@ const createBlockFromHTML = ( type = "heading"; break; } + case "H4": { + headingLevel = 4; + type = "heading"; + break; + } case "DIV": { type = "card"; break; diff --git a/components/FontLoader.tsx b/components/FontLoader.tsx new file mode 100644 index 00000000..3de78a07 --- /dev/null +++ b/components/FontLoader.tsx @@ -0,0 +1,121 @@ +// Server-side font loading component +// Following Google's best practices: https://web.dev/articles/font-best-practices +// - Preconnect to font origins for early connection +// - Use font-display: swap (shows fallback immediately, swaps when ready) +// - Don't block rendering - some FOUT is acceptable and better UX than invisible text + +import { + getFontConfig, + generateFontFaceCSS, + getFontPreloadLinks, + getGoogleFontsUrl, + getFontFamilyValue, + getFontBaseSize, +} from "src/fonts"; + +type FontLoaderProps = { + headingFontId: string | undefined; + bodyFontId: string | undefined; +}; + +export function FontLoader({ headingFontId, bodyFontId }: FontLoaderProps) { + const headingFont = getFontConfig(headingFontId); + const bodyFont = getFontConfig(bodyFontId); + + // Collect all unique fonts to load + const fontsToLoad = headingFont.id === bodyFont.id + ? [headingFont] + : [headingFont, bodyFont]; + + // Collect preload links (deduplicated) + const preloadLinksSet = new Set(); + const preloadLinks: { href: string; type: string }[] = []; + for (const font of fontsToLoad) { + for (const link of getFontPreloadLinks(font)) { + if (!preloadLinksSet.has(link.href)) { + preloadLinksSet.add(link.href); + preloadLinks.push(link); + } + } + } + + // Collect font-face CSS + const fontFaceCSS = fontsToLoad + .map((font) => generateFontFaceCSS(font)) + .filter(Boolean) + .join("\n\n"); + + // Collect Google Fonts URLs (deduplicated) + const googleFontsUrls = [...new Set( + fontsToLoad + .map((font) => getGoogleFontsUrl(font)) + .filter((url): url is string => url !== null) + )]; + + const headingFontValue = getFontFamilyValue(headingFont); + const bodyFontValue = getFontFamilyValue(bodyFont); + const bodyFontBaseSize = getFontBaseSize(bodyFont); + + // Set font CSS variables scoped to .leafletWrapper so they don't affect app UI + const fontVariableCSS = ` +.leafletWrapper { + --theme-heading-font: ${headingFontValue}; + --theme-font: ${bodyFontValue}; + --theme-font-base-size: ${bodyFontBaseSize}px; +} +`.trim(); + + return ( + <> + {/* + Google Fonts best practice: preconnect to both origins + - fonts.googleapis.com serves the CSS + - fonts.gstatic.com serves the font files (needs crossorigin for CORS) + Place these as early as possible in + */} + {googleFontsUrls.length > 0 && ( + <> + + + {googleFontsUrls.map((url) => ( + + ))} + + )} + {/* Preload local font files for early discovery */} + {preloadLinks.map((link) => ( + + ))} + {/* @font-face declarations (for local fonts) and CSS variable */} +