Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 17 additions & 1 deletion apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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"
},
Expand Down
68 changes: 14 additions & 54 deletions apps/web/src/components/admin/settings/branding/theme-preview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, string> = {
'"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) */
Expand All @@ -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 && <link rel="stylesheet" href={googleFontsUrl} />}
<div
className="rounded-lg border overflow-hidden"
style={
{
...cssVars,
backgroundColor: 'var(--background)',
borderColor: 'var(--border)',
color: 'var(--foreground)',
fontFamily,
} as React.CSSProperties
}
>
<PortalPreview />
</div>
</>
<div
className="rounded-lg border overflow-hidden"
style={
{
...cssVars,
backgroundColor: 'var(--background)',
borderColor: 'var(--border)',
color: 'var(--foreground)',
fontFamily,
} as React.CSSProperties
}
>
<PortalPreview />
</div>
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,134 +7,116 @@ import {
generateReadableCSS,
parseCssToMinimal,
replaceCssVar,
normalizeFontSans,
type ThemeConfig,
type MinimalThemeVariables,
type ThemeMode,
type ParsedCssVariables,
} 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

Expand Down Expand Up @@ -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]
)

Expand Down
7 changes: 1 addition & 6 deletions apps/web/src/components/auth/portal-auth-shell.tsx
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -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 (
<div className="relative min-h-screen flex items-center justify-center px-4 py-12 overflow-hidden">
{googleFontsUrl && <link rel="stylesheet" href={googleFontsUrl} />}
{themeStyles && <style dangerouslySetInnerHTML={{ __html: themeStyles }} />}
{customCss && <style dangerouslySetInnerHTML={{ __html: customCss }} />}
<div
Expand Down
72 changes: 69 additions & 3 deletions apps/web/src/globals.css
Original file line number Diff line number Diff line change
@@ -1,10 +1,76 @@
@import "tailwindcss";
@plugin "@tailwindcss/typography";
@import "tw-animate-css";
/* Self-hosted Inter (variable, wght 100..900). Bundled from npm so no
fonts.googleapis.com <link> exists for a CDN (Cloudflare Fonts) to
rewrite — see the widget hydration / CDN-rewrite notes. */
/* Self-hosted fonts: every font referenced by the branding picker (FONT_OPTIONS in
use-branding-state.ts) AND the theme presets (FONTS in presets.ts) is bundled here,
so no fonts.googleapis.com <link> is emitted. An edge optimizer (e.g. Cloudflare
Fonts) rewrites such a link into inline @font-face and breaks SSR hydration — add
fonts here, never via a Google Fonts <link>, and keep the family name matching the
value those two sources store. Inter also loads variable for the default UI; Lato
ships 400/700 only; Geist's @fontsource family is "Geist Sans". */
@import "@fontsource-variable/inter/index.css";
@import "@fontsource/inter/400.css";
@import "@fontsource/inter/500.css";
@import "@fontsource/inter/600.css";
@import "@fontsource/inter/700.css";
@import "@fontsource/roboto/400.css";
@import "@fontsource/roboto/500.css";
@import "@fontsource/roboto/600.css";
@import "@fontsource/roboto/700.css";
@import "@fontsource/open-sans/400.css";
@import "@fontsource/open-sans/500.css";
@import "@fontsource/open-sans/600.css";
@import "@fontsource/open-sans/700.css";
@import "@fontsource/lato/400.css";
@import "@fontsource/lato/700.css";
@import "@fontsource/nunito/400.css";
@import "@fontsource/nunito/500.css";
@import "@fontsource/nunito/600.css";
@import "@fontsource/nunito/700.css";
@import "@fontsource/poppins/400.css";
@import "@fontsource/poppins/500.css";
@import "@fontsource/poppins/600.css";
@import "@fontsource/poppins/700.css";
@import "@fontsource/dm-sans/400.css";
@import "@fontsource/dm-sans/500.css";
@import "@fontsource/dm-sans/600.css";
@import "@fontsource/dm-sans/700.css";
@import "@fontsource/plus-jakarta-sans/400.css";
@import "@fontsource/plus-jakarta-sans/500.css";
@import "@fontsource/plus-jakarta-sans/600.css";
@import "@fontsource/plus-jakarta-sans/700.css";
@import "@fontsource/manrope/400.css";
@import "@fontsource/manrope/500.css";
@import "@fontsource/manrope/600.css";
@import "@fontsource/manrope/700.css";
@import "@fontsource/space-grotesk/400.css";
@import "@fontsource/space-grotesk/500.css";
@import "@fontsource/space-grotesk/600.css";
@import "@fontsource/space-grotesk/700.css";
@import "@fontsource/playfair-display/400.css";
@import "@fontsource/playfair-display/500.css";
@import "@fontsource/playfair-display/600.css";
@import "@fontsource/playfair-display/700.css";
@import "@fontsource/merriweather/400.css";
@import "@fontsource/merriweather/500.css";
@import "@fontsource/merriweather/600.css";
@import "@fontsource/merriweather/700.css";
@import "@fontsource/lora/400.css";
@import "@fontsource/lora/500.css";
@import "@fontsource/lora/600.css";
@import "@fontsource/lora/700.css";
@import "@fontsource/fira-code/400.css";
@import "@fontsource/fira-code/500.css";
@import "@fontsource/fira-code/600.css";
@import "@fontsource/fira-code/700.css";
@import "@fontsource/jetbrains-mono/400.css";
@import "@fontsource/jetbrains-mono/500.css";
@import "@fontsource/jetbrains-mono/600.css";
@import "@fontsource/jetbrains-mono/700.css";
@import "@fontsource/geist-sans/400.css";
@import "@fontsource/geist-sans/500.css";
@import "@fontsource/geist-sans/600.css";
@import "@fontsource/geist-sans/700.css";
Comment thread
mortondev marked this conversation as resolved.
Comment thread
mortondev marked this conversation as resolved.

@custom-variant dark (&:is(.dark *));

Expand Down
Loading