Skip to content
Open
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
20 changes: 16 additions & 4 deletions apps/desktop/src/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,18 @@
src: url('./fonts/JetBrainsMono-Italic.woff2') format('woff2');
}

/* Prefer a symbols-only Nerd Font when one is installed, then use a full Nerd
Font only for its private-use glyphs. The unicode range prevents these local
faces from changing ordinary UI typography or color emoji. */
@font-face {
font-family: 'Hermes Nerd Glyphs';
font-display: swap;
src:
local('Symbols Nerd Font Mono'), local('Symbols Nerd Font'), local('JetBrainsMono Nerd Font Mono'),
local('JetBrainsMono Nerd Font');
unicode-range: U+E000-F8FF, U+F0000-F2FFF;
}

@theme inline {
--color-background: var(--dt-background);
--color-foreground: var(--dt-foreground);
Expand Down Expand Up @@ -388,16 +400,16 @@
--dt-user-bubble-border: var(--ui-stroke-tertiary);

--dt-font-sans:
'Segoe WPC', 'Segoe UI', -apple-system, BlinkMacSystemFont, 'SF Pro Text', system-ui, sans-serif,
'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji', emoji;
'Hermes Nerd Glyphs', 'Segoe WPC', 'Segoe UI', -apple-system, BlinkMacSystemFont, 'SF Pro Text', system-ui,
sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji', emoji;
/* Key caps always use the native UI face β€” never theme typography overrides. */
--dt-font-kbd: -apple-system, BlinkMacSystemFont, 'SF Pro Text', 'Segoe UI', system-ui, sans-serif;
/* Menlo/Monaco first β€” Apple's native monospace faces β€” so code/diff read
in the system mono on macOS, with SF Mono and bundled Courier Prime as
fallbacks. */
--dt-font-mono:
Menlo, Monaco, 'SF Mono', 'Courier Prime', monospace, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol',
'Noto Color Emoji', emoji;
'Hermes Nerd Glyphs', Menlo, Monaco, 'SF Mono', 'Courier Prime', monospace, 'Apple Color Emoji',
'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji', emoji;
--dt-base-size: 1rem;
--dt-line-height: 1.5;
--dt-letter-spacing: 0;
Expand Down
11 changes: 9 additions & 2 deletions apps/desktop/src/themes/context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import { $activeGatewayProfile, normalizeProfileKey } from '@/store/profile'

import { $backendThemes, $pendingSkinApply } from './backend-sync'
import { hexToRgb, mix, readableOn } from './color'
import { BUILTIN_THEME_LIST, DEFAULT_SKIN_NAME, DEFAULT_TYPOGRAPHY, nousTheme } from './presets'
import { BUILTIN_THEME_LIST, DEFAULT_SKIN_NAME, DEFAULT_TYPOGRAPHY, nousTheme, withNerdGlyphFallback } from './presets'
import type { DesktopTheme, DesktopThemeColors } from './types'
import { $userThemes, listAllThemes, resolveTheme } from './user-themes'

Expand Down Expand Up @@ -180,7 +180,14 @@ function applyTheme(theme: DesktopTheme, mode: 'light' | 'dark') {

const root = document.documentElement
const c = theme.colors
const typo = { ...DEFAULT_TYPOGRAPHY, ...nousTheme.typography, ...theme.typography }
const configuredTypography = { ...DEFAULT_TYPOGRAPHY, ...nousTheme.typography, ...theme.typography }

const typo = {
...configuredTypography,
fontSans: withNerdGlyphFallback(configuredTypography.fontSans),
fontMono: withNerdGlyphFallback(configuredTypography.fontMono)
}

const rendered = renderedModeFor(c, mode)
const isDark = rendered === 'dark'
const midground = c.midground ?? c.ring
Expand Down
20 changes: 19 additions & 1 deletion apps/desktop/src/themes/presets.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { describe, expect, it } from 'vitest'

import { BUILTIN_THEME_LIST, DEFAULT_TYPOGRAPHY, EMOJI_FALLBACK } from './presets'
import { BUILTIN_THEME_LIST, DEFAULT_TYPOGRAPHY, EMOJI_FALLBACK, NERD_GLYPH_FALLBACK, withNerdGlyphFallback } from './presets'

// #40364: none of the UI text/mono fonts carry emoji glyphs, so every font
// stack must end with a color-emoji fallback or emoji render as tofu on
Expand Down Expand Up @@ -30,4 +30,22 @@ describe('theme typography emoji fallback (#40364)', () => {
expect(EMOJI_FALLBACK).toContain('Segoe UI Emoji')
expect(EMOJI_FALLBACK).toContain('Noto Color Emoji')
})

it('keeps Nerd Font icon fallbacks available to the UI', () => {
expect(DEFAULT_TYPOGRAPHY.fontSans).toContain(NERD_GLYPH_FALLBACK)
expect(DEFAULT_TYPOGRAPHY.fontMono).toContain(NERD_GLYPH_FALLBACK)
})

it('adds the private-use fallback to external theme typography without changing its preferred stack', () => {
expect(withNerdGlyphFallback('ui-monospace, monospace')).toBe(
`${NERD_GLYPH_FALLBACK}, ui-monospace, monospace`
)
})

it('moves and deduplicates the private-use fallback ahead of external theme fonts', () => {
expect(withNerdGlyphFallback(`ui-monospace, ${NERD_GLYPH_FALLBACK}, monospace`)).toBe(
`${NERD_GLYPH_FALLBACK}, ui-monospace, monospace`
)
expect(withNerdGlyphFallback('')).toBe(NERD_GLYPH_FALLBACK)
})
})
28 changes: 24 additions & 4 deletions apps/desktop/src/themes/presets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,34 @@ import type { DesktopTheme, DesktopThemeTypography } from './types'
// Covers macOS, Windows, Linux, plus the `emoji` generic for anything else.
export const EMOJI_FALLBACK = '"Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji", emoji'

// `Hermes Nerd Glyphs` is a unicode-range-limited @font-face declared in
// styles.css. It only participates for Nerd Font private-use codepoints, so a
// locally installed full Nerd Font cannot change ordinary text metrics.
export const NERD_GLYPH_FALLBACK = '"Hermes Nerd Glyphs"'

const SYSTEM_SANS =
'"Segoe WPC", "Segoe UI", -apple-system, BlinkMacSystemFont, "SF Pro Text", "SF Pro Display", system-ui, sans-serif, ' +
NERD_GLYPH_FALLBACK +
', "Segoe WPC", "Segoe UI", -apple-system, BlinkMacSystemFont, "SF Pro Text", "SF Pro Display", system-ui, sans-serif, ' +
EMOJI_FALLBACK

const SYSTEM_MONO = 'Menlo, Monaco, "SF Mono", "Courier Prime", monospace, ' + EMOJI_FALLBACK
const SYSTEM_MONO =
NERD_GLYPH_FALLBACK + ', Menlo, Monaco, "SF Mono", "Courier Prime", monospace, ' + EMOJI_FALLBACK

export const DEFAULT_TYPOGRAPHY: DesktopThemeTypography = { fontSans: SYSTEM_SANS, fontMono: SYSTEM_MONO }

/**
* Give an external theme the same private-use glyph fallback as built-in
* themes. The face's unicode range prevents it from affecting normal text.
*/
export function withNerdGlyphFallback(fontFamily: string): string {
const withoutFallback = fontFamily
.trim()
.replace(/(?:^|,\s*)(?:"Hermes Nerd Glyphs"|'Hermes Nerd Glyphs'|Hermes Nerd Glyphs)(?=\s*(?:,|$))/gi, '')
.replace(/^,\s*|,\s*$/g, '')

return withoutFallback ? `${NERD_GLYPH_FALLBACK}, ${withoutFallback}` : NERD_GLYPH_FALLBACK
}

const NOUS_BLUE = '#0053FD'
const PSYCHE_BLUE = '#1540B1'
const PSYCHE_WARM = '#FFE6CB'
Expand Down Expand Up @@ -235,8 +255,8 @@ export const cyberpunkTheme: DesktopTheme = {
userBubbleBorder: '#004800'
},
typography: {
fontMono: `"Courier New", Courier, monospace, ${EMOJI_FALLBACK}`,
fontSans: `"Courier New", Courier, monospace, ${EMOJI_FALLBACK}`
fontMono: `${NERD_GLYPH_FALLBACK}, "Courier New", Courier, monospace, ${EMOJI_FALLBACK}`,
fontSans: `${NERD_GLYPH_FALLBACK}, "Courier New", Courier, monospace, ${EMOJI_FALLBACK}`
}
}

Expand Down