From b917d82f7ea1da47f76fb0c0e4783ade23d8751d Mon Sep 17 00:00:00 2001 From: Cyril Date: Mon, 13 Apr 2026 16:55:11 +0200 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8(frontend)=20apply=20font=20preference?= =?UTF-8?q?=20to=20app=20layout?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Hook, CSS variable and LiveKit integration for custom fonts. --- src/frontend/src/hooks/useApplyUiFont.ts | 48 ++++++++++++++++++++++++ src/frontend/src/layout/Layout.tsx | 3 ++ src/frontend/src/stores/accessibility.ts | 23 +++--------- src/frontend/src/styles/index.css | 5 +++ src/frontend/src/styles/livekit.css | 2 +- 5 files changed, 62 insertions(+), 19 deletions(-) create mode 100644 src/frontend/src/hooks/useApplyUiFont.ts diff --git a/src/frontend/src/hooks/useApplyUiFont.ts b/src/frontend/src/hooks/useApplyUiFont.ts new file mode 100644 index 00000000..b5542ecf --- /dev/null +++ b/src/frontend/src/hooks/useApplyUiFont.ts @@ -0,0 +1,48 @@ +import { useEffect } from 'react' +import { useSnapshot } from 'valtio' +import { + accessibilityStore, + UI_FONT_OVERRIDES, + UiFont, +} from '@/stores/accessibility' + +const fontImports: Partial Promise>> = { + lexend: () => import('@fontsource-variable/lexend'), + 'atkinson-hyperlegible': () => + import('@fontsource-variable/atkinson-hyperlegible-next'), + opendyslexic: () => import('@fontsource/opendyslexic'), +} + +const loadedFonts = new Set() + +export function useApplyUiFont() { + const { uiFont } = useSnapshot(accessibilityStore) + + useEffect(() => { + const loader = fontImports[uiFont] + if (loader && !loadedFonts.has(uiFont)) { + loader() + .then(() => loadedFonts.add(uiFont)) + .catch((err) => { + console.error( + `[useApplyUiFont] Failed to load font "${uiFont}":`, + err + ) + }) + } + + const override = UI_FONT_OVERRIDES[uiFont] + if (override) { + document.documentElement.style.setProperty( + '--app-font-family', + override + ) + } else { + document.documentElement.style.removeProperty('--app-font-family') + } + + return () => { + document.documentElement.style.removeProperty('--app-font-family') + } + }, [uiFont]) +} diff --git a/src/frontend/src/layout/Layout.tsx b/src/frontend/src/layout/Layout.tsx index 38e657ea..04e8fe4c 100644 --- a/src/frontend/src/layout/Layout.tsx +++ b/src/frontend/src/layout/Layout.tsx @@ -6,6 +6,7 @@ import { useSnapshot } from 'valtio' import { Footer } from '@/layout/Footer' import { ScreenReaderAnnouncer } from '@/primitives' import { SkipLink, MAIN_CONTENT_ID } from './SkipLink' +import { useApplyUiFont } from '@/hooks/useApplyUiFont' export type Layout = 'fullpage' | 'centered' @@ -19,6 +20,7 @@ export const Layout = ({ children }: { children: ReactNode }) => { const layoutSnap = useSnapshot(layoutStore) const showHeader = layoutSnap.showHeader const showFooter = layoutSnap.showFooter + useApplyUiFont() return ( <> @@ -30,6 +32,7 @@ export const Layout = ({ children }: { children: ReactNode }) => { backgroundColor: 'white', color: 'default.text', flex: '1', + fontFamily: 'var(--app-font-family)', })} style={{ height: !showFooter ? '100%' : undefined, diff --git a/src/frontend/src/stores/accessibility.ts b/src/frontend/src/stores/accessibility.ts index bd4b5a0f..f6221da8 100644 --- a/src/frontend/src/stores/accessibility.ts +++ b/src/frontend/src/stores/accessibility.ts @@ -15,24 +15,11 @@ export const UI_FONT_OPTIONS: UiFont[] = [ 'opendyslexic', ] -const SYSTEM_SANS_STACK = [ - 'ui-sans-serif', - 'system-ui', - '-apple-system', - 'BlinkMacSystemFont', - '"Segoe UI"', - 'Roboto', - '"Helvetica Neue"', - 'Arial', - '"Noto Sans"', - 'sans-serif', -].join(', ') - -export const UI_FONT_STACKS: Record = { - default: SYSTEM_SANS_STACK, - lexend: `"Lexend Variable", ${SYSTEM_SANS_STACK}`, - 'atkinson-hyperlegible': `"Atkinson Hyperlegible Next", ${SYSTEM_SANS_STACK}`, - opendyslexic: `OpenDyslexic, ${SYSTEM_SANS_STACK}`, +export const UI_FONT_OVERRIDES: Partial> = { + lexend: '"Lexend Variable", var(--app-font-family-base)', + 'atkinson-hyperlegible': + '"Atkinson Hyperlegible Next Variable", var(--app-font-family-base)', + opendyslexic: 'OpenDyslexic, var(--app-font-family-base)', } export type CaptionTextSize = 'small' | 'medium' | 'large' diff --git a/src/frontend/src/styles/index.css b/src/frontend/src/styles/index.css index 39504da3..e3499a3d 100644 --- a/src/frontend/src/styles/index.css +++ b/src/frontend/src/styles/index.css @@ -6,6 +6,11 @@ body, height: 100%; } +:root { + --app-font-family-base: var(--fonts-sans); + --app-font-family: var(--app-font-family-base); +} + .sr-only { position: absolute; width: 1px; diff --git a/src/frontend/src/styles/livekit.css b/src/frontend/src/styles/livekit.css index 8044f036..816c42d7 100644 --- a/src/frontend/src/styles/livekit.css +++ b/src/frontend/src/styles/livekit.css @@ -38,7 +38,7 @@ --lk-control-bar-height: 69px; --lk-chat-header-height: 69px; - --lk-font-family: var(--fonts-sans); + --lk-font-family: var(--app-font-family); --lk-font-size: 1rem; --lk-bg6: #6b7280; --lk-control-border-width: 1px;