Просмотр исходного кода

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>
ali akrem barka 1 год назад
Родитель
Сommit
787b46688b

+ 8 - 0
index.html

@@ -40,6 +40,14 @@
       content="https://shadcn-admin.netlify.app/images/shadcn-admin.png"
     />
 
+    <!-- font family -->
+    <link rel="preconnect" href="https://fonts.googleapis.com" />
+    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
+    <link
+      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"
+      rel="stylesheet"
+    />
+
     <meta name="theme-color" content="#fff" />
   </head>
   <body class="group/body">

+ 28 - 0
src/config/fonts.ts

@@ -0,0 +1,28 @@
+/**
+ * List of available font names (visit the url`/settings/appearance`).
+ * This array is used to generate Tailwind's `safelist` inside 'tailwind.config.js' and 'appearance-form.tsx'
+ * to prevent dynamic font classes (e.g., `font-inter`, `font-manrope`) from being removed during purging.
+ *
+ * 📝 How to Add a New Font:
+ * 1. Add the font name here.
+ * 2. Update the `<link>` tag in 'index.html' to include the new font from Google Fonts (or any other source).
+ * 3. Add new fontFamily 'tailwind.config.js'
+ *
+ * Example:
+ * fonts.ts           → Add 'roboto' to this array.
+ * index.html         → Add Google Fonts link for Roboto.
+ * tailwind.config.js  → Add the new font inside `theme.extend.fontFamily`.
+ * ```ts
+ * theme: {
+ *   // other configs
+ *   extend: {
+ *      fontFamily: {
+ *        inter: ['Inter', ...fontFamily.sans],
+ *        manrope: ['Manrope', ...fontFamily.sans],
+ *        roboto: ['Roboto', ...fontFamily.sans], // Add new font here
+ *      }
+ *   }
+ * }
+ * ```
+ */
+export const fonts = ['inter', 'manrope', 'system'] as const

+ 48 - 0
src/context/font-context.tsx

@@ -0,0 +1,48 @@
+import React, { createContext, useContext, useEffect, useState } from 'react'
+import { fonts } from '@/config/fonts'
+
+type Font = (typeof fonts)[number]
+
+interface FontContextType {
+  font: Font
+  setFont: (font: Font) => void
+}
+
+const FontContext = createContext<FontContextType | undefined>(undefined)
+
+export const FontProvider: React.FC<{ children: React.ReactNode }> = ({
+  children,
+}) => {
+  const [font, _setFont] = useState<Font>(() => {
+    const savedFont = localStorage.getItem('font')
+    return fonts.includes(savedFont as Font) ? (savedFont as Font) : fonts[0]
+  })
+
+  useEffect(() => {
+    const applyFont = (font: string) => {
+      const root = document.documentElement
+      root.classList.forEach((cls) => {
+        if (cls.startsWith('font-')) root.classList.remove(cls)
+      })
+      root.classList.add(`font-${font}`)
+    }
+
+    applyFont(font)
+  }, [font])
+
+  const setFont = (font: Font) => {
+    localStorage.setItem('font', font)
+    _setFont(font)
+  }
+
+  return <FontContext value={{ font, setFont }}>{children}</FontContext>
+}
+
+// eslint-disable-next-line react-refresh/only-export-components
+export const useFont = () => {
+  const context = useContext(FontContext)
+  if (!context) {
+    throw new Error('useFont must be used within a FontProvider')
+  }
+  return context
+}

+ 23 - 11
src/features/settings/appearance/appearance-form.tsx

@@ -2,7 +2,10 @@ import { z } from 'zod'
 import { useForm } from 'react-hook-form'
 import { ChevronDownIcon } from '@radix-ui/react-icons'
 import { zodResolver } from '@hookform/resolvers/zod'
+import { fonts } from '@/config/fonts'
 import { cn } from '@/lib/utils'
+import { useFont } from '@/context/font-context'
+import { useTheme } from '@/context/theme-context'
 import { toast } from '@/hooks/use-toast'
 import { Button, buttonVariants } from '@/components/ui/button'
 import {
@@ -20,7 +23,7 @@ const appearanceFormSchema = z.object({
   theme: z.enum(['light', 'dark'], {
     required_error: 'Please select a theme.',
   }),
-  font: z.enum(['inter', 'manrope', 'system'], {
+  font: z.enum(fonts, {
     invalid_type_error: 'Select a font',
     required_error: 'Please select a font.',
   }),
@@ -28,18 +31,25 @@ const appearanceFormSchema = z.object({
 
 type AppearanceFormValues = z.infer<typeof appearanceFormSchema>
 
-// This can come from your database or API.
-const defaultValues: Partial<AppearanceFormValues> = {
-  theme: 'light',
-}
-
 export function AppearanceForm() {
+  const { font, setFont } = useFont()
+  const { theme, setTheme } = useTheme()
+
+  // This can come from your database or API.
+  const defaultValues: Partial<AppearanceFormValues> = {
+    theme: theme as 'light' | 'dark',
+    font,
+  }
+
   const form = useForm<AppearanceFormValues>({
     resolver: zodResolver(appearanceFormSchema),
     defaultValues,
   })
 
   function onSubmit(data: AppearanceFormValues) {
+    if (data.font != font) setFont(data.font)
+    if (data.theme != theme) setTheme(data.theme)
+
     toast({
       title: 'You submitted the following values:',
       description: (
@@ -64,18 +74,20 @@ export function AppearanceForm() {
                   <select
                     className={cn(
                       buttonVariants({ variant: 'outline' }),
-                      'w-[200px] appearance-none font-normal'
+                      'w-[200px] appearance-none font-normal capitalize'
                     )}
                     {...field}
                   >
-                    <option value='inter'>Inter</option>
-                    <option value='manrope'>Manrope</option>
-                    <option value='system'>System</option>
+                    {fonts.map((font) => (
+                      <option key={font} value={font}>
+                        {font}
+                      </option>
+                    ))}
                   </select>
                 </FormControl>
                 <ChevronDownIcon className='absolute right-3 top-2.5 h-4 w-4 opacity-50' />
               </div>
-              <FormDescription>
+              <FormDescription className='font-manrope'>
                 Set the font you want to use in the dashboard.
               </FormDescription>
               <FormMessage />

+ 4 - 1
src/main.tsx

@@ -10,6 +10,7 @@ import { RouterProvider, createRouter } from '@tanstack/react-router'
 import { useAuthStore } from '@/stores/authStore'
 import { handleServerError } from '@/utils/handle-server-error'
 import { toast } from '@/hooks/use-toast'
+import { FontProvider } from './context/font-context'
 import { ThemeProvider } from './context/theme-context'
 import './index.css'
 // Generated Routes
@@ -98,7 +99,9 @@ if (!rootElement.innerHTML) {
     <StrictMode>
       <QueryClientProvider client={queryClient}>
         <ThemeProvider defaultTheme='light' storageKey='vite-ui-theme'>
-          <RouterProvider router={router} />
+          <FontProvider>
+            <RouterProvider router={router} />
+          </FontProvider>
         </ThemeProvider>
       </QueryClientProvider>
     </StrictMode>

+ 7 - 0
tailwind.config.js

@@ -1,9 +1,12 @@
 import tailwindCssAnimate from 'tailwindcss-animate'
+import { fontFamily } from 'tailwindcss/defaultTheme'
+import { fonts } from './src/config/fonts'
 
 /** @type {import('tailwindcss').Config} */
 export default {
   darkMode: ['class'],
   content: ['./index.html', './src/**/*.{ts,tsx,js,jsx}'],
+  safelist: fonts.map((font) => `font-${font}`),
   theme: {
     container: {
       center: 'true',
@@ -13,6 +16,10 @@ export default {
       },
     },
     extend: {
+      fontFamily: {
+        inter: ['Inter', ...fontFamily.sans],
+        manrope: ['Manrope', ...fontFamily.sans],
+      },
       borderRadius: {
         lg: 'var(--radius)',
         md: 'calc(var(--radius) - 2px)',