diff --git a/apps/web/package.json b/apps/web/package.json index 42f471cca..abf698f77 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -26,6 +26,22 @@ "@dnd-kit/utilities": "^3.2.2", "@floating-ui/dom": "^1.7.6", "@fontsource-variable/inter": "^5.2.8", + "@fontsource/dm-sans": "^5.2.8", + "@fontsource/fira-code": "^5.2.7", + "@fontsource/geist-sans": "^5.2.5", + "@fontsource/inter": "^5.2.8", + "@fontsource/jetbrains-mono": "^5.2.8", + "@fontsource/lato": "^5.2.7", + "@fontsource/lora": "^5.2.8", + "@fontsource/manrope": "^5.2.8", + "@fontsource/merriweather": "^5.2.11", + "@fontsource/nunito": "^5.2.7", + "@fontsource/open-sans": "^5.2.7", + "@fontsource/playfair-display": "^5.2.8", + "@fontsource/plus-jakarta-sans": "^5.2.8", + "@fontsource/poppins": "^5.2.7", + "@fontsource/roboto": "^5.2.10", + "@fontsource/space-grotesk": "^5.2.10", "@heroicons/react": "^2.2.0", "@hookform/resolvers": "^5.2.2", "@modelcontextprotocol/sdk": "^1.29.0", @@ -118,7 +134,7 @@ "tw-animate-css": "^1.4.0", "typeid-js": "^1.2.0", "yaml": "^2.8.4", - "zod": "^4.3.6", + "zod": "4.3.6", "zod-openapi": "^5.4.6", "zustand": "^5.0.12" }, diff --git a/apps/web/src/components/admin/settings/branding/theme-preview.tsx b/apps/web/src/components/admin/settings/branding/theme-preview.tsx index f6e22625a..47002ceb3 100644 --- a/apps/web/src/components/admin/settings/branding/theme-preview.tsx +++ b/apps/web/src/components/admin/settings/branding/theme-preview.tsx @@ -13,42 +13,6 @@ import { import type { ParsedCssVariables } from '@/lib/shared/theme' import { cn } from '@/lib/shared/utils' -/** Map font family names to Google Fonts URL */ -const GOOGLE_FONT_MAP: Record = { - '"Inter"': 'Inter', - '"Roboto"': 'Roboto', - '"Open Sans"': 'Open+Sans', - '"Lato"': 'Lato', - '"Montserrat"': 'Montserrat', - '"Poppins"': 'Poppins', - '"Nunito"': 'Nunito', - '"DM Sans"': 'DM+Sans', - '"Plus Jakarta Sans"': 'Plus+Jakarta+Sans', - '"Geist"': 'Geist', - '"Work Sans"': 'Work+Sans', - '"Raleway"': 'Raleway', - '"Source Sans 3"': 'Source+Sans+3', - '"Outfit"': 'Outfit', - '"Manrope"': 'Manrope', - '"Space Grotesk"': 'Space+Grotesk', - '"Playfair Display"': 'Playfair+Display', - '"Merriweather"': 'Merriweather', - '"Lora"': 'Lora', - '"Crimson Text"': 'Crimson+Text', - '"Fira Code"': 'Fira+Code', - '"JetBrains Mono"': 'JetBrains+Mono', -} - -function getGoogleFontsUrl(fontFamily: string | undefined): string | null { - if (!fontFamily) return null - for (const [cssName, googleName] of Object.entries(GOOGLE_FONT_MAP)) { - if (fontFamily.includes(cssName)) { - return `https://fonts.googleapis.com/css2?family=${googleName}:wght@400;500;600;700&display=swap` - } - } - return null -} - interface ThemePreviewProps { previewMode: 'light' | 'dark' /** Parsed CSS variables from the theme CSS (source of truth) */ @@ -72,26 +36,22 @@ export function ThemePreview({ previewMode, cssVariables }: ThemePreviewProps) { const cssVars = useMemo(() => ({ ...modeVars, ...COMPONENT_ALIASES }), [modeVars]) const fontFamily = modeVars['--font-sans'] || DEFAULT_FONT - const googleFontsUrl = useMemo(() => getGoogleFontsUrl(fontFamily), [fontFamily]) return ( - <> - {googleFontsUrl && } -
- -
- +
+ +
) } diff --git a/apps/web/src/components/admin/settings/branding/use-branding-state.ts b/apps/web/src/components/admin/settings/branding/use-branding-state.ts index f44fdfa62..340a0a53e 100644 --- a/apps/web/src/components/admin/settings/branding/use-branding-state.ts +++ b/apps/web/src/components/admin/settings/branding/use-branding-state.ts @@ -7,6 +7,7 @@ import { generateReadableCSS, parseCssToMinimal, replaceCssVar, + normalizeFontSans, type ThemeConfig, type MinimalThemeVariables, type ThemeMode, @@ -14,127 +15,108 @@ import { } from '@/lib/shared/theme' import { useSaveBrandingTheme } from '@/lib/client/mutations/settings' +// Each `value` family must be self-hosted in globals.css (matching the @fontsource +// @font-face family name), or the selection falls back to the generic stack. export const FONT_OPTIONS = [ { id: 'inter', name: 'Inter', value: '"Inter", ui-sans-serif, system-ui, sans-serif', category: 'Sans Serif', - googleName: 'Inter', }, { id: 'system', name: 'System UI', value: 'ui-sans-serif, system-ui, -apple-system, sans-serif', category: 'System', - googleName: null, }, { id: 'roboto', name: 'Roboto', value: '"Roboto", ui-sans-serif, system-ui, sans-serif', category: 'Sans Serif', - googleName: 'Roboto', }, { id: 'open-sans', name: 'Open Sans', value: '"Open Sans", ui-sans-serif, system-ui, sans-serif', category: 'Sans Serif', - googleName: 'Open+Sans', }, { id: 'lato', name: 'Lato', value: '"Lato", ui-sans-serif, system-ui, sans-serif', category: 'Sans Serif', - googleName: 'Lato', }, { id: 'poppins', name: 'Poppins', value: '"Poppins", ui-sans-serif, system-ui, sans-serif', category: 'Sans Serif', - googleName: 'Poppins', }, { id: 'dm-sans', name: 'DM Sans', value: '"DM Sans", ui-sans-serif, system-ui, sans-serif', category: 'Sans Serif', - googleName: 'DM+Sans', }, { id: 'jakarta', name: 'Plus Jakarta Sans', value: '"Plus Jakarta Sans", ui-sans-serif, system-ui, sans-serif', category: 'Sans Serif', - googleName: 'Plus+Jakarta+Sans', }, { id: 'geist', name: 'Geist', - value: '"Geist", ui-sans-serif, system-ui, sans-serif', + // @fontsource publishes Geist as the "Geist Sans" family (see globals.css). + value: '"Geist Sans", ui-sans-serif, system-ui, sans-serif', category: 'Sans Serif', - googleName: 'Geist', }, { id: 'manrope', name: 'Manrope', value: '"Manrope", ui-sans-serif, system-ui, sans-serif', category: 'Sans Serif', - googleName: 'Manrope', }, { id: 'space-grotesk', name: 'Space Grotesk', value: '"Space Grotesk", ui-sans-serif, system-ui, sans-serif', category: 'Sans Serif', - googleName: 'Space+Grotesk', }, { id: 'playfair', name: 'Playfair Display', value: '"Playfair Display", ui-serif, Georgia, serif', category: 'Serif', - googleName: 'Playfair+Display', }, { id: 'merriweather', name: 'Merriweather', value: '"Merriweather", ui-serif, Georgia, serif', category: 'Serif', - googleName: 'Merriweather', }, { id: 'lora', name: 'Lora', value: '"Lora", ui-serif, Georgia, serif', category: 'Serif', - googleName: 'Lora', }, { id: 'fira-code', name: 'Fira Code', value: '"Fira Code", ui-monospace, monospace', category: 'Monospace', - googleName: 'Fira+Code', }, { id: 'jetbrains', name: 'JetBrains Mono', value: '"JetBrains Mono", ui-monospace, monospace', category: 'Monospace', - googleName: 'JetBrains+Mono', }, ] as const -export const ALL_FONTS_URL = `https://fonts.googleapis.com/css2?family=${FONT_OPTIONS.filter( - (f) => f.googleName -) - .map((f) => f.googleName) - .join('&family=')}&display=swap` - const DEFAULT_FONT = '"Inter", ui-sans-serif, system-ui, sans-serif' const DEFAULT_RADIUS = 0.625 @@ -247,7 +229,7 @@ export function useBrandingState(options: UseBrandingStateOptions): BrandingStat ) const currentFontId = useMemo( - () => FONT_OPTIONS.find((f) => f.value === font)?.id || 'inter', + () => FONT_OPTIONS.find((f) => f.value === normalizeFontSans(font))?.id || 'inter', [font] ) diff --git a/apps/web/src/components/auth/portal-auth-shell.tsx b/apps/web/src/components/auth/portal-auth-shell.tsx index 8401e5c2a..cef35cc38 100644 --- a/apps/web/src/components/auth/portal-auth-shell.tsx +++ b/apps/web/src/components/auth/portal-auth-shell.tsx @@ -1,7 +1,7 @@ import { useMemo } from 'react' import { useRouteContext } from '@tanstack/react-router' import { PortalBrandMark } from './portal-brand-mark' -import { generateThemeCSS, getGoogleFontsUrl } from '@/lib/shared/theme' +import { generateThemeCSS } from '@/lib/shared/theme' import type { BrandingConfig } from '@/lib/server/domains/settings/settings.types' interface PortalAuthShellProps { @@ -38,14 +38,9 @@ export function PortalAuthShell({ heading, subheading, children, footer }: Porta const hasThemeConfig = brandingConfig.light || brandingConfig.dark return hasThemeConfig ? generateThemeCSS(brandingConfig) : '' }, [brandingConfig]) - const googleFontsUrl = useMemo( - () => (brandingConfig ? getGoogleFontsUrl(brandingConfig) : null), - [brandingConfig] - ) return (
- {googleFontsUrl && } {themeStyles &&