From bc4cc80581db8bfc52f2e048fb6426f57a9fae88 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Thu, 26 Feb 2026 14:57:41 -0600 Subject: [PATCH] Simplify theme fonts to a single font (remove heading/body roles) Reduce font complexity from two-role (title/body) to a single font that applies globally to all text. Updates NIP.md f tag format from ["f", family, role, url] to ["f", family, url], removes ThemeFonts wrapper in favor of a single ThemeFont on ThemeConfig.font, and simplifies FontPicker to a single dropdown. --- NIP.md | 25 +++--- src/components/AppProvider.tsx | 10 +-- src/components/FontPicker.tsx | 139 +++++++++------------------------ src/hooks/usePublishTheme.ts | 34 +++----- src/lib/fontLoader.ts | 61 ++++----------- src/lib/schemas.ts | 8 +- src/lib/themeEvent.ts | 62 ++++++--------- src/pages/ProfilePage.tsx | 14 ++-- src/themes.ts | 16 +--- 9 files changed, 115 insertions(+), 254 deletions(-) diff --git a/NIP.md b/NIP.md index bd6b19f2..536d0d36 100644 --- a/NIP.md +++ b/NIP.md @@ -28,8 +28,7 @@ A theme consists of colors, optional fonts, and an optional background. Colors a ["c", "#1a1a2e", "background"], ["c", "#e0e0e0", "text"], ["c", "#6c3ce0", "primary"], - ["f", "Playfair Display", "title", "https://example.com/playfair.woff2"], - ["f", "Inter", "body", "https://example.com/inter.woff2"], + ["f", "Inter", "https://example.com/inter.woff2"], ["bg", "url https://example.com/bg.jpg", "mode cover", "m image/jpeg", "dim 1920x1080"], ["title", "MK Dark Theme"], ["alt", "Custom theme: MK Dark Theme"] @@ -47,7 +46,7 @@ The `content` field is unused and MUST be an empty string (`""`). |---------|----------|---------------------------------------------------------------------------------------| | `d` | Yes | Unique identifier (slug) for this theme, e.g. `"mk-dark-theme"` | | `c` | Yes (×3) | Hex color with marker. See [Color Tags](#color-tags). | -| `f` | No | Font declaration. See [Font Tags](#font-tags). | +| `f` | No | Font declaration. See [Font Tag](#font-tag). | | `bg` | No | Background media. See [Background Tag](#background-tag). | | `title` | Yes | Human-readable theme name | | `alt` | Yes | NIP-31 human-readable fallback | @@ -74,8 +73,7 @@ Replaceable event that represents the user's currently active profile theme. Onl ["c", "#1a1a2e", "background"], ["c", "#e0e0e0", "text"], ["c", "#6c3ce0", "primary"], - ["f", "Playfair Display", "title", "https://example.com/playfair.woff2"], - ["f", "Inter", "body", "https://example.com/inter.woff2"], + ["f", "Inter", "https://example.com/inter.woff2"], ["bg", "url https://example.com/bg.jpg", "mode cover", "m image/jpeg"], ["title", "MK Dark Theme"], ["alt", "Active profile theme"] @@ -92,7 +90,7 @@ The `content` field is unused and MUST be an empty string (`""`). | Tag | Required | Description | |---------|----------|---------------------------------------------------------------------------------------| | `c` | Yes (×3) | Hex color with marker. See [Color Tags](#color-tags). | -| `f` | No | Font declaration. See [Font Tags](#font-tags). | +| `f` | No | Font declaration. See [Font Tag](#font-tag). | | `bg` | No | Background media. See [Background Tag](#background-tag). | | `title` | No | Human-readable name for the theme | | `alt` | Yes | NIP-31 human-readable fallback | @@ -123,20 +121,19 @@ Format: `["c", "#rrggbb", ""]` - All three markers (`"primary"`, `"text"`, `"background"`) MUST be present. - Only one `c` tag per marker is allowed. -### Font Tags +### Font Tag -Format: `["f", "", "", ""]` +Format: `["f", "", ""]` | Index | Required | Description | |-------|----------|-----------------------------------------------------------------------------------------------| | 0 | Yes | Tag name: `"f"` | -| 1 | Yes | CSS `font-family` name (e.g. `"Playfair Display"`) | -| 2 | Yes | Role marker: `"title"` or `"body"` | -| 3 | Yes | Direct URL to a font file (`.woff2`, `.ttf`, `.otf`) | +| 1 | Yes | CSS `font-family` name (e.g. `"Inter"`) | +| 2 | Yes | Direct URL to a font file (`.woff2`, `.ttf`, `.otf`) | -- Both `f` tags are optional on the event. -- At most one `f` tag per role is allowed. -- `"title"` fonts apply to headings and display text. `"body"` fonts apply to body/paragraph text. +- The `f` tag is optional on the event. +- At most one `f` tag per event is allowed. +- The font applies globally to all text (body, headings, UI elements). - If the URL fails to load, the client SHOULD fall back to a default font gracefully. - Variable font files (covering multiple weights in a single file) are preferred. diff --git a/src/components/AppProvider.tsx b/src/components/AppProvider.tsx index 70359bd9..32d8b420 100644 --- a/src/components/AppProvider.tsx +++ b/src/components/AppProvider.tsx @@ -4,7 +4,7 @@ import { useLocalStorage } from '@/hooks/useLocalStorage'; import { AppContext, type AppConfig, type AppContextType, type Theme, type RelayMetadata } from '@/contexts/AppContext'; import { builtinThemes, themePresets, buildThemeCssFromCore, coreToTokens, buildThemeCss, resolveTheme, type ThemeConfig } from '@/themes'; import { ThemeSchemaCompat, ThemeConfigCompatSchema, FeedSettingsSchema, ContentWarningPolicySchema } from '@/lib/schemas'; -import { loadAndApplyFonts } from '@/lib/fontLoader'; +import { loadAndApplyFont } from '@/lib/fontLoader'; interface AppProviderProps { children: ReactNode; @@ -171,11 +171,11 @@ function useApplyTheme(theme: Theme, customTheme: ThemeConfig | undefined) { function useApplyFonts(theme: Theme, customTheme: ThemeConfig | undefined) { useEffect(() => { const resolved = resolveTheme(theme); - if (resolved === 'custom' && customTheme?.fonts) { - loadAndApplyFonts(customTheme.fonts); + if (resolved === 'custom' && customTheme?.font) { + loadAndApplyFont(customTheme.font); } else { // Clear any custom font overrides when switching to a builtin theme - loadAndApplyFonts(undefined); + loadAndApplyFont(undefined); } - }, [theme, customTheme?.fonts]); + }, [theme, customTheme?.font]); } diff --git a/src/components/FontPicker.tsx b/src/components/FontPicker.tsx index 34061216..b0eda32b 100644 --- a/src/components/FontPicker.tsx +++ b/src/components/FontPicker.tsx @@ -7,9 +7,7 @@ import { Button } from '@/components/ui/button'; import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from '@/components/ui/command'; import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'; import { bundledFonts, loadBundledFont, type FontCategory } from '@/lib/fonts'; -import type { ThemeFont, ThemeFonts } from '@/themes'; - -type FontRole = 'title' | 'body'; +import type { ThemeFont } from '@/themes'; /** Category labels for UI display */ const CATEGORY_LABELS: Record = { @@ -46,38 +44,55 @@ function usePreloadFonts(open: boolean) { }, [open]); } -/** Single-role font picker (title or body). */ -function RolePicker({ label, value, onChange }: { - label: string; - value: ThemeFont | undefined; - onChange: (font: ThemeFont | undefined) => void; -}) { +/** + * Font picker component for selecting a single custom font. + * Integrates with the theme system via useTheme().applyCustomTheme(). + */ +export function FontPicker() { + const { customTheme, applyCustomTheme } = useTheme(); const [open, setOpen] = useState(false); const [search, setSearch] = useState(''); usePreloadFonts(open); + const currentFont: ThemeFont | undefined = customTheme?.font; + const handleSelect = (family: string) => { - if (value?.family === family) { - onChange(undefined); + if (currentFont?.family === family) { + // Deselect + handleReset(); } else { - onChange({ family }); + const currentColors = customTheme?.colors ?? { + background: '228 20% 10%', + text: '210 40% 98%', + primary: '258 70% 60%', + }; + applyCustomTheme({ + ...customTheme, + colors: currentColors, + font: { family }, + }); } setOpen(false); setSearch(''); }; const handleReset = () => { - onChange(undefined); + if (!customTheme) return; + applyCustomTheme({ + ...customTheme, + font: undefined, + }); }; return ( -
+
- - {label} - - {value && ( +

+ + Font +

+ {currentFont && ( @@ -127,7 +142,7 @@ function RolePicker({ label, value, onChange }: { {font.family} @@ -140,10 +155,10 @@ function RolePicker({ label, value, onChange }: { - {value && ( + {currentFont && (

The quick brown fox jumps over the lazy dog.

@@ -151,81 +166,3 @@ function RolePicker({ label, value, onChange }: {
); } - -/** - * Font picker component with separate title/body selections. - * Integrates with the theme system via useTheme().applyCustomTheme(). - */ -export function FontPicker() { - const { customTheme, applyCustomTheme } = useTheme(); - - /** Current fonts from the custom theme, if any. */ - const currentFonts: ThemeFonts | undefined = customTheme?.fonts; - - const handleFontChange = (role: FontRole, font: ThemeFont | undefined) => { - const currentColors = customTheme?.colors ?? { - background: '228 20% 10%', - text: '210 40% 98%', - primary: '258 70% 60%', - }; - - const newFonts: ThemeFonts = { - ...currentFonts, - [role]: font, - }; - - // Clean up: if both roles are undefined, don't store empty fonts object - const hasFonts = newFonts.title || newFonts.body; - - applyCustomTheme({ - ...customTheme, - colors: currentColors, - fonts: hasFonts ? newFonts : undefined, - }); - }; - - const hasAnyFont = currentFonts?.title || currentFonts?.body; - - const handleResetAll = () => { - if (!customTheme) return; - applyCustomTheme({ - ...customTheme, - fonts: undefined, - }); - }; - - return ( -
-
-

- - Fonts -

- {hasAnyFont && ( - - )} -
- -
- handleFontChange('title', font)} - /> - handleFontChange('body', font)} - /> -
-
- ); -} diff --git a/src/hooks/usePublishTheme.ts b/src/hooks/usePublishTheme.ts index 1651b5c5..5f9af7ba 100644 --- a/src/hooks/usePublishTheme.ts +++ b/src/hooks/usePublishTheme.ts @@ -1,7 +1,7 @@ import { useCallback } from 'react'; import { useQueryClient } from '@tanstack/react-query'; -import type { ThemeConfig, ThemeFonts } from '@/themes'; +import type { ThemeConfig } from '@/themes'; import { useCurrentUser } from './useCurrentUser'; import { useNostrPublish } from './useNostrPublish'; import { @@ -14,36 +14,20 @@ import { } from '@/lib/themeEvent'; import { resolveFontUrl } from '@/lib/fontLoader'; -/** - * Hook to publish theme-related Nostr events. - * - * - `publishTheme`: Publish a kind 33891 theme definition (create or update) - * - `setActiveTheme`: Publish a kind 11667 active profile theme - * - `deleteTheme`: Publish a kind 5 deletion for a theme definition - * - `clearActiveTheme`: Publish a kind 5 deletion for the active profile theme - */ /** * Resolve font URLs for Nostr publishing. * Bundled fonts get CDN URLs, others keep their existing URL. */ function resolveThemeForPublishing(config: ThemeConfig): ThemeConfig { - if (!config.fonts) return config; + if (!config.font) return config; - const resolved: ThemeFonts = {}; - if (config.fonts.title) { - resolved.title = { - family: config.fonts.title.family, - url: resolveFontUrl(config.fonts.title.family, config.fonts.title.url), - }; - } - if (config.fonts.body) { - resolved.body = { - family: config.fonts.body.family, - url: resolveFontUrl(config.fonts.body.family, config.fonts.body.url), - }; - } - - return { ...config, fonts: resolved }; + return { + ...config, + font: { + family: config.font.family, + url: resolveFontUrl(config.font.family, config.font.url), + }, + }; } export function usePublishTheme() { diff --git a/src/lib/fontLoader.ts b/src/lib/fontLoader.ts index 77f79143..f11bf703 100644 --- a/src/lib/fontLoader.ts +++ b/src/lib/fontLoader.ts @@ -5,10 +5,10 @@ * 1. Bundled fontsource packages (via dynamic import) * 2. Remote URLs (via @font-face injection) * - * Also manages the CSS overrides that apply title/body fonts to the document. + * Also manages the CSS override that applies a custom font to the document. */ -import type { ThemeFonts } from '@/themes'; +import type { ThemeFont } from '@/themes'; import { findBundledFont, loadBundledFont } from '@/lib/fonts'; // ─── @font-face injection for remote fonts ──────────────────────────── @@ -53,17 +53,16 @@ const FONT_OVERRIDE_STYLE_ID = 'theme-font-overrides'; const DEFAULT_FONT_STACK = '"Inter Variable", Inter, system-ui, sans-serif'; /** - * Apply font-family overrides to the document. - * - Title font applies to headings (h1–h6) and elements with data-font="title". - * - Body font applies to the html element (cascades to everything else). + * Apply a font-family override to the document. + * The font applies globally to the html element (cascades to everything). * - * Pass undefined to clear all overrides. + * Pass undefined to clear the override. */ -export function applyFontOverrides(fonts: ThemeFonts | undefined): void { +export function applyFontOverride(font: ThemeFont | undefined): void { let style = document.getElementById(FONT_OVERRIDE_STYLE_ID) as HTMLStyleElement | null; - if (!fonts || (!fonts.title && !fonts.body)) { - // No custom fonts — remove overrides + if (!font) { + // No custom font — remove override style?.remove(); return; } @@ -74,24 +73,7 @@ export function applyFontOverrides(fonts: ThemeFonts | undefined): void { document.head.appendChild(style); } - let css = ''; - - if (fonts.body) { - css += `html { font-family: "${fonts.body.family}", ${DEFAULT_FONT_STACK} !important; }\n`; - } - - if (fonts.title) { - css += `h1, h2, h3, h4, h5, h6, [data-font="title"] { font-family: "${fonts.title.family}", ${DEFAULT_FONT_STACK} !important; }\n`; - } - - style.textContent = css; -} - -/** - * Remove all font overrides and injected @font-face rules. - */ -export function clearFontOverrides(): void { - document.getElementById(FONT_OVERRIDE_STYLE_ID)?.remove(); + style.textContent = `html { font-family: "${font.family}", ${DEFAULT_FONT_STACK} !important; }\n`; } // ─── High-level font loading ────────────────────────────────────────── @@ -112,28 +94,17 @@ export async function loadFont(family: string, url?: string): Promise { } /** - * Load all fonts specified in a ThemeFonts config and apply CSS overrides. - * This is the main entry point for applying theme fonts. + * Load a theme font and apply the CSS override. + * This is the main entry point for applying a theme font. */ -export async function loadAndApplyFonts(fonts: ThemeFonts | undefined): Promise { - if (!fonts) { - applyFontOverrides(undefined); +export async function loadAndApplyFont(font: ThemeFont | undefined): Promise { + if (!font) { + applyFontOverride(undefined); return; } - // Load fonts in parallel - const loads: Promise[] = []; - if (fonts.title) { - loads.push(loadFont(fonts.title.family, fonts.title.url)); - } - if (fonts.body) { - loads.push(loadFont(fonts.body.family, fonts.body.url)); - } - - await Promise.allSettled(loads); - - // Apply CSS overrides after fonts are loaded - applyFontOverrides(fonts); + await loadFont(font.family, font.url); + applyFontOverride(font); } /** diff --git a/src/lib/schemas.ts b/src/lib/schemas.ts index a7ed559d..ec7294a0 100644 --- a/src/lib/schemas.ts +++ b/src/lib/schemas.ts @@ -70,11 +70,7 @@ export const ThemeFontSchema = z.object({ url: z.string().optional(), }); -/** Zod schema for ThemeFonts */ -export const ThemeFontsSchema = z.object({ - title: ThemeFontSchema.optional(), - body: ThemeFontSchema.optional(), -}); + /** Zod schema for ThemeBackground */ export const ThemeBackgroundSchema = z.object({ @@ -89,7 +85,7 @@ export const ThemeBackgroundSchema = z.object({ export const ThemeConfigSchema = z.object({ title: z.string().optional(), colors: CoreThemeColorsSchema, - fonts: ThemeFontsSchema.optional(), + font: ThemeFontSchema.optional(), background: ThemeBackgroundSchema.optional(), }); diff --git a/src/lib/themeEvent.ts b/src/lib/themeEvent.ts index c36bdd3a..37a0e947 100644 --- a/src/lib/themeEvent.ts +++ b/src/lib/themeEvent.ts @@ -1,5 +1,5 @@ import type { NostrEvent } from '@nostrify/nostrify'; -import type { CoreThemeColors, ThemeConfig, ThemeFont, ThemeFonts, ThemeBackground } from '@/themes'; +import type { CoreThemeColors, ThemeConfig, ThemeFont, ThemeBackground } from '@/themes'; import { hslStringToHex, hexToHslString } from '@/lib/colorUtils'; // ─── Kind Constants ─────────────────────────────────────────────────── @@ -48,39 +48,23 @@ function parseColorTags(tags: string[][]): CoreThemeColors | null { // ─── Font Tag Helpers ───────────────────────────────────────────────── -/** Build `f` tags from ThemeFonts. */ -function buildFontTags(fonts: ThemeFonts | undefined): string[][] { - if (!fonts) return []; - const tags: string[][] = []; - if (fonts.title?.family) { - const tag = ['f', fonts.title.family, 'title']; - if (fonts.title.url) tag.push(fonts.title.url); - tags.push(tag); - } - if (fonts.body?.family) { - const tag = ['f', fonts.body.family, 'body']; - if (fonts.body.url) tag.push(fonts.body.url); - tags.push(tag); - } - return tags; +/** Build an `f` tag from a ThemeFont. */ +function buildFontTag(font: ThemeFont | undefined): string[][] { + if (!font?.family) return []; + const tag = ['f', font.family]; + if (font.url) tag.push(font.url); + return [tag]; } -/** Parse `f` tags into ThemeFonts. Returns undefined if no font tags. */ -function parseFontTags(tags: string[][]): ThemeFonts | undefined { - let title: ThemeFont | undefined; - let body: ThemeFont | undefined; - +/** Parse the first `f` tag into a ThemeFont. Returns undefined if no f tag. */ +function parseFontTag(tags: string[][]): ThemeFont | undefined { for (const tag of tags) { - if (tag[0] !== 'f' || !tag[1] || !tag[2]) continue; + if (tag[0] !== 'f' || !tag[1]) continue; const font: ThemeFont = { family: tag[1] }; - if (tag[3]) font.url = tag[3]; - - if (tag[2] === 'title' && !title) title = font; - if (tag[2] === 'body' && !body) body = font; + if (tag[2]) font.url = tag[2]; + return font; } - - if (!title && !body) return undefined; - return { title, body }; + return undefined; } // ─── Background Tag Helpers ─────────────────────────────────────────── @@ -135,8 +119,8 @@ export interface ThemeDefinition { description?: string; /** The 3 core theme colors */ colors: CoreThemeColors; - /** Optional font selections */ - fonts?: ThemeFonts; + /** Optional custom font */ + font?: ThemeFont; /** Optional background */ background?: ThemeBackground; /** The original Nostr event */ @@ -170,10 +154,10 @@ export function parseThemeDefinition(event: NostrEvent): ThemeDefinition | null if (!colors) return null; - const fonts = parseFontTags(event.tags); + const font = parseFontTag(event.tags); const background = parseBackgroundTag(event.tags); - return { identifier, title, description, colors, fonts, background, event }; + return { identifier, title, description, colors, font, background, event }; } /** Create tags for a kind 33891 theme definition event. */ @@ -186,7 +170,7 @@ export function buildThemeDefinitionTags( const tags: string[][] = [ ['d', identifier], ...buildColorTags(themeConfig.colors), - ...buildFontTags(themeConfig.fonts), + ...buildFontTag(themeConfig.font), ...buildBackgroundTag(themeConfig.background), ['title', title], ['alt', `Custom theme: ${title}`], @@ -214,8 +198,8 @@ export function titleToSlug(title: string): string { export interface ActiveProfileTheme { /** The 3 core theme colors */ colors: CoreThemeColors; - /** Optional font selections */ - fonts?: ThemeFonts; + /** Optional custom font */ + font?: ThemeFont; /** Optional background */ background?: ThemeBackground; /** naddr-style reference to the source theme definition, if any */ @@ -243,11 +227,11 @@ export function parseActiveProfileTheme(event: NostrEvent): ActiveProfileTheme | if (!colors) return null; - const fonts = parseFontTags(event.tags); + const font = parseFontTag(event.tags); const background = parseBackgroundTag(event.tags); const sourceRef = event.tags.find(([n]) => n === 'a')?.[1]; - return { colors, fonts, background, sourceRef, event }; + return { colors, font, background, sourceRef, event }; } /** Create tags for a kind 11667 active profile theme event. */ @@ -258,7 +242,7 @@ export function buildActiveThemeTags( ): string[][] { const tags: string[][] = [ ...buildColorTags(themeConfig.colors), - ...buildFontTags(themeConfig.fonts), + ...buildFontTag(themeConfig.font), ...buildBackgroundTag(themeConfig.background), ['alt', 'Active profile theme'], ]; diff --git a/src/pages/ProfilePage.tsx b/src/pages/ProfilePage.tsx index 4ba104e7..73c68f9a 100644 --- a/src/pages/ProfilePage.tsx +++ b/src/pages/ProfilePage.tsx @@ -45,7 +45,7 @@ import { useLocalStorage } from '@/hooks/useLocalStorage'; import { useFeedSettings } from '@/hooks/useFeedSettings'; import { useEncryptedSettings } from '@/hooks/useEncryptedSettings'; import { buildThemeCssFromCore, coreToTokens, buildThemeCss, builtinThemes, resolveTheme } from '@/themes'; -import { loadAndApplyFonts } from '@/lib/fontLoader'; +import { loadAndApplyFont } from '@/lib/fontLoader'; import { cn, STICKY_HEADER_CLASS } from '@/lib/utils'; import type { FeedItem } from '@/lib/feedUtils'; import type { NostrEvent } from '@nostrify/nostrify'; @@ -740,7 +740,7 @@ export function ProfilePage() { // Temporarily apply the visited user's theme globally while on their profile const { theme: ownTheme, customTheme: ownCustomTheme } = useTheme(); - const profileThemeFonts = showCustomProfileThemes ? profileTheme?.fonts : undefined; + const profileThemeFont = showCustomProfileThemes ? profileTheme?.font : undefined; useEffect(() => { if (!profileThemeColors) return; @@ -756,8 +756,8 @@ export function ProfilePage() { const previousCss = el.textContent; el.textContent = css; - // Apply profile fonts (if any) - loadAndApplyFonts(profileThemeFonts); + // Apply profile font (if any) + loadAndApplyFont(profileThemeFont); // Restore the user's own theme on cleanup return () => { @@ -772,10 +772,10 @@ export function ProfilePage() { styleEl.textContent = buildThemeCss(coreToTokens(colors)); } } - // Restore own fonts or clear overrides - loadAndApplyFonts(ownCustomTheme?.fonts); + // Restore own font or clear override + loadAndApplyFont(ownCustomTheme?.font); }; - }, [profileThemeColors, profileThemeFonts]); // eslint-disable-line react-hooks/exhaustive-deps + }, [profileThemeColors, profileThemeFont]); // eslint-disable-line react-hooks/exhaustive-deps const pinnedIds = useMemo(() => supplementary?.pinnedIds ?? [], [supplementary?.pinnedIds]); diff --git a/src/themes.ts b/src/themes.ts index fff3e89e..241e20ff 100644 --- a/src/themes.ts +++ b/src/themes.ts @@ -24,14 +24,6 @@ export interface ThemeFont { url?: string; } -/** Font selections for title and body roles. */ -export interface ThemeFonts { - /** Font for headings and display text */ - title?: ThemeFont; - /** Font for body/paragraph text */ - body?: ThemeFont; -} - // ─── Background Types ───────────────────────────────────────────────── /** Background image/video configuration. */ @@ -60,8 +52,8 @@ export interface ThemeConfig { title?: string; /** The 3 core colors */ colors: CoreThemeColors; - /** Optional font selections */ - fonts?: ThemeFonts; + /** Optional custom font (applies globally to all text) */ + font?: ThemeFont; /** Optional background media */ background?: ThemeBackground; } @@ -120,8 +112,8 @@ export interface ThemePreset { featured?: boolean; /** The 3 core colors. */ colors: CoreThemeColors; - /** Optional font selections for this preset. */ - fonts?: ThemeFonts; + /** Optional custom font for this preset. */ + font?: ThemeFont; /** Optional background for this preset. */ background?: ThemeBackground; }