Skip to content

Commit 787b466

Browse files
AliAkremsatnaing
andauthored
feat: allow changing font family in setting
* add appearance features * feat: add Google Fonts integration and refactor font handling * chore: format codes * refactor: improve type safety and configurability - update formatting to fix Prettier issues - improve type safety and allow easier configuration - add safelist inside `tailwind.config.js` to prevent font-related class purging --------- Co-authored-by: satnaing <satnaingdev@gmail.com>
1 parent 611c186 commit 787b466

File tree

6 files changed

+118
-12
lines changed

6 files changed

+118
-12
lines changed

index.html

+8
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,14 @@
4040
content="https://shadcn-admin.netlify.app/images/shadcn-admin.png"
4141
/>
4242

43+
<!-- font family -->
44+
<link rel="preconnect" href="https://fonts.googleapis.com" />
45+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
46+
<link
47+
href="https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&family=Manrope:wght@200..800&display=swap"
48+
rel="stylesheet"
49+
/>
50+
4351
<meta name="theme-color" content="#fff" />
4452
</head>
4553
<body class="group/body">

src/config/fonts.ts

+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
/**
2+
* List of available font names (visit the url`/settings/appearance`).
3+
* This array is used to generate Tailwind's `safelist` inside 'tailwind.config.js' and 'appearance-form.tsx'
4+
* to prevent dynamic font classes (e.g., `font-inter`, `font-manrope`) from being removed during purging.
5+
*
6+
* 📝 How to Add a New Font:
7+
* 1. Add the font name here.
8+
* 2. Update the `<link>` tag in 'index.html' to include the new font from Google Fonts (or any other source).
9+
* 3. Add new fontFamily 'tailwind.config.js'
10+
*
11+
* Example:
12+
* fonts.ts → Add 'roboto' to this array.
13+
* index.html → Add Google Fonts link for Roboto.
14+
* tailwind.config.js → Add the new font inside `theme.extend.fontFamily`.
15+
* ```ts
16+
* theme: {
17+
* // other configs
18+
* extend: {
19+
* fontFamily: {
20+
* inter: ['Inter', ...fontFamily.sans],
21+
* manrope: ['Manrope', ...fontFamily.sans],
22+
* roboto: ['Roboto', ...fontFamily.sans], // Add new font here
23+
* }
24+
* }
25+
* }
26+
* ```
27+
*/
28+
export const fonts = ['inter', 'manrope', 'system'] as const

src/context/font-context.tsx

+48
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import React, { createContext, useContext, useEffect, useState } from 'react'
2+
import { fonts } from '@/config/fonts'
3+
4+
type Font = (typeof fonts)[number]
5+
6+
interface FontContextType {
7+
font: Font
8+
setFont: (font: Font) => void
9+
}
10+
11+
const FontContext = createContext<FontContextType | undefined>(undefined)
12+
13+
export const FontProvider: React.FC<{ children: React.ReactNode }> = ({
14+
children,
15+
}) => {
16+
const [font, _setFont] = useState<Font>(() => {
17+
const savedFont = localStorage.getItem('font')
18+
return fonts.includes(savedFont as Font) ? (savedFont as Font) : fonts[0]
19+
})
20+
21+
useEffect(() => {
22+
const applyFont = (font: string) => {
23+
const root = document.documentElement
24+
root.classList.forEach((cls) => {
25+
if (cls.startsWith('font-')) root.classList.remove(cls)
26+
})
27+
root.classList.add(`font-${font}`)
28+
}
29+
30+
applyFont(font)
31+
}, [font])
32+
33+
const setFont = (font: Font) => {
34+
localStorage.setItem('font', font)
35+
_setFont(font)
36+
}
37+
38+
return <FontContext value={{ font, setFont }}>{children}</FontContext>
39+
}
40+
41+
// eslint-disable-next-line react-refresh/only-export-components
42+
export const useFont = () => {
43+
const context = useContext(FontContext)
44+
if (!context) {
45+
throw new Error('useFont must be used within a FontProvider')
46+
}
47+
return context
48+
}

src/features/settings/appearance/appearance-form.tsx

+23-11
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@ import { z } from 'zod'
22
import { useForm } from 'react-hook-form'
33
import { ChevronDownIcon } from '@radix-ui/react-icons'
44
import { zodResolver } from '@hookform/resolvers/zod'
5+
import { fonts } from '@/config/fonts'
56
import { cn } from '@/lib/utils'
7+
import { useFont } from '@/context/font-context'
8+
import { useTheme } from '@/context/theme-context'
69
import { toast } from '@/hooks/use-toast'
710
import { Button, buttonVariants } from '@/components/ui/button'
811
import {
@@ -20,26 +23,33 @@ const appearanceFormSchema = z.object({
2023
theme: z.enum(['light', 'dark'], {
2124
required_error: 'Please select a theme.',
2225
}),
23-
font: z.enum(['inter', 'manrope', 'system'], {
26+
font: z.enum(fonts, {
2427
invalid_type_error: 'Select a font',
2528
required_error: 'Please select a font.',
2629
}),
2730
})
2831

2932
type AppearanceFormValues = z.infer<typeof appearanceFormSchema>
3033

31-
// This can come from your database or API.
32-
const defaultValues: Partial<AppearanceFormValues> = {
33-
theme: 'light',
34-
}
35-
3634
export function AppearanceForm() {
35+
const { font, setFont } = useFont()
36+
const { theme, setTheme } = useTheme()
37+
38+
// This can come from your database or API.
39+
const defaultValues: Partial<AppearanceFormValues> = {
40+
theme: theme as 'light' | 'dark',
41+
font,
42+
}
43+
3744
const form = useForm<AppearanceFormValues>({
3845
resolver: zodResolver(appearanceFormSchema),
3946
defaultValues,
4047
})
4148

4249
function onSubmit(data: AppearanceFormValues) {
50+
if (data.font != font) setFont(data.font)
51+
if (data.theme != theme) setTheme(data.theme)
52+
4353
toast({
4454
title: 'You submitted the following values:',
4555
description: (
@@ -64,18 +74,20 @@ export function AppearanceForm() {
6474
<select
6575
className={cn(
6676
buttonVariants({ variant: 'outline' }),
67-
'w-[200px] appearance-none font-normal'
77+
'w-[200px] appearance-none font-normal capitalize'
6878
)}
6979
{...field}
7080
>
71-
<option value='inter'>Inter</option>
72-
<option value='manrope'>Manrope</option>
73-
<option value='system'>System</option>
81+
{fonts.map((font) => (
82+
<option key={font} value={font}>
83+
{font}
84+
</option>
85+
))}
7486
</select>
7587
</FormControl>
7688
<ChevronDownIcon className='absolute right-3 top-2.5 h-4 w-4 opacity-50' />
7789
</div>
78-
<FormDescription>
90+
<FormDescription className='font-manrope'>
7991
Set the font you want to use in the dashboard.
8092
</FormDescription>
8193
<FormMessage />

src/main.tsx

+4-1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { RouterProvider, createRouter } from '@tanstack/react-router'
1010
import { useAuthStore } from '@/stores/authStore'
1111
import { handleServerError } from '@/utils/handle-server-error'
1212
import { toast } from '@/hooks/use-toast'
13+
import { FontProvider } from './context/font-context'
1314
import { ThemeProvider } from './context/theme-context'
1415
import './index.css'
1516
// Generated Routes
@@ -98,7 +99,9 @@ if (!rootElement.innerHTML) {
9899
<StrictMode>
99100
<QueryClientProvider client={queryClient}>
100101
<ThemeProvider defaultTheme='light' storageKey='vite-ui-theme'>
101-
<RouterProvider router={router} />
102+
<FontProvider>
103+
<RouterProvider router={router} />
104+
</FontProvider>
102105
</ThemeProvider>
103106
</QueryClientProvider>
104107
</StrictMode>

tailwind.config.js

+7
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
import tailwindCssAnimate from 'tailwindcss-animate'
2+
import { fontFamily } from 'tailwindcss/defaultTheme'
3+
import { fonts } from './src/config/fonts'
24

35
/** @type {import('tailwindcss').Config} */
46
export default {
57
darkMode: ['class'],
68
content: ['./index.html', './src/**/*.{ts,tsx,js,jsx}'],
9+
safelist: fonts.map((font) => `font-${font}`),
710
theme: {
811
container: {
912
center: 'true',
@@ -13,6 +16,10 @@ export default {
1316
},
1417
},
1518
extend: {
19+
fontFamily: {
20+
inter: ['Inter', ...fontFamily.sans],
21+
manrope: ['Manrope', ...fontFamily.sans],
22+
},
1623
borderRadius: {
1724
lg: 'var(--radius)',
1825
md: 'calc(var(--radius) - 2px)',

0 commit comments

Comments
 (0)