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.
This commit is contained in:
Alex Gleason
2026-02-26 14:57:41 -06:00
parent fbcd40e164
commit bc4cc80581
9 changed files with 115 additions and 254 deletions
+11 -14
View File
@@ -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", "<marker>"]`
- All three markers (`"primary"`, `"text"`, `"background"`) MUST be present.
- Only one `c` tag per marker is allowed.
### Font Tags
### Font Tag
Format: `["f", "<family>", "<role>", "<url>"]`
Format: `["f", "<family>", "<url>"]`
| 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.
+5 -5
View File
@@ -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]);
}
+38 -101
View File
@@ -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<FontCategory, string> = {
@@ -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 (
<div className="space-y-2">
<div className="space-y-3 rounded-xl border border-border bg-card p-4">
<div className="flex items-center justify-between">
<span className="text-xs font-medium text-muted-foreground uppercase tracking-wider">
{label}
</span>
{value && (
<h3 className="text-xs font-semibold uppercase tracking-wider text-muted-foreground/70 flex items-center gap-2">
<Type className="size-3.5" />
Font
</h3>
{currentFont && (
<Button
variant="ghost"
size="sm"
@@ -97,9 +112,9 @@ function RolePicker({ label, value, onChange }: {
role="combobox"
aria-expanded={open}
className="w-full justify-between font-normal h-9 text-sm"
style={value ? { fontFamily: `"${value.family}", sans-serif` } : undefined}
style={currentFont ? { fontFamily: `"${currentFont.family}", sans-serif` } : undefined}
>
{value?.family ?? 'Default (Inter)'}
{currentFont?.family ?? 'Default (Inter)'}
<ChevronsUpDown className="ml-2 size-4 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
@@ -127,7 +142,7 @@ function RolePicker({ label, value, onChange }: {
<Check
className={cn(
'mr-2 size-4',
value?.family === font.family ? 'opacity-100' : 'opacity-0',
currentFont?.family === font.family ? 'opacity-100' : 'opacity-0',
)}
/>
{font.family}
@@ -140,10 +155,10 @@ function RolePicker({ label, value, onChange }: {
</PopoverContent>
</Popover>
{value && (
{currentFont && (
<p
className="text-sm text-muted-foreground"
style={{ fontFamily: `"${value.family}", sans-serif` }}
style={{ fontFamily: `"${currentFont.family}", sans-serif` }}
>
The quick brown fox jumps over the lazy dog.
</p>
@@ -151,81 +166,3 @@ function RolePicker({ label, value, onChange }: {
</div>
);
}
/**
* 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 (
<div className="space-y-3 rounded-xl border border-border bg-card p-4">
<div className="flex items-center justify-between">
<h3 className="text-xs font-semibold uppercase tracking-wider text-muted-foreground/70 flex items-center gap-2">
<Type className="size-3.5" />
Fonts
</h3>
{hasAnyFont && (
<Button
variant="ghost"
size="sm"
onClick={handleResetAll}
className="h-6 gap-1 text-xs text-muted-foreground hover:text-foreground px-2"
>
<RotateCcw className="size-3" />
Reset all
</Button>
)}
</div>
<div className="space-y-4">
<RolePicker
label="Heading Font"
value={currentFonts?.title}
onChange={(font) => handleFontChange('title', font)}
/>
<RolePicker
label="Body Font"
value={currentFonts?.body}
onChange={(font) => handleFontChange('body', font)}
/>
</div>
</div>
);
}
+9 -25
View File
@@ -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() {
+16 -45
View File
@@ -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 (h1h6) 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<void> {
}
/**
* 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<void> {
if (!fonts) {
applyFontOverrides(undefined);
export async function loadAndApplyFont(font: ThemeFont | undefined): Promise<void> {
if (!font) {
applyFontOverride(undefined);
return;
}
// Load fonts in parallel
const loads: Promise<void>[] = [];
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);
}
/**
+2 -6
View File
@@ -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(),
});
+23 -39
View File
@@ -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'],
];
+7 -7
View File
@@ -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]);
+4 -12
View File
@@ -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;
}