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:
@@ -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.
|
||||
|
||||
|
||||
@@ -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
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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
@@ -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<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
@@ -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
@@ -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'],
|
||||
];
|
||||
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user