From b60838c041c450547d808c2da6c4fd00268986d1 Mon Sep 17 00:00:00 2001 From: Leonardo Montini Date: Wed, 6 Aug 2025 18:21:31 +0200 Subject: [PATCH 1/7] feat: handle theme with localStorage --- src/components/ThemeToggle.tsx | 167 +++++++++++++++++--------------- src/routes/__root.tsx | 26 +---- src/routes/_libraries/route.tsx | 2 +- tailwind.config.cjs | 9 +- 4 files changed, 100 insertions(+), 104 deletions(-) diff --git a/src/components/ThemeToggle.tsx b/src/components/ThemeToggle.tsx index 22a480da3..2b08e32bb 100644 --- a/src/components/ThemeToggle.tsx +++ b/src/components/ThemeToggle.tsx @@ -1,95 +1,84 @@ -import { createServerFn } from '@tanstack/react-start' -import { getCookie, setCookie } from '@tanstack/react-start/server' +import { ScriptOnce } from '@tanstack/react-router' import * as React from 'react' import { FaMoon, FaSun } from 'react-icons/fa' import { twMerge } from 'tailwind-merge' - import { z } from 'zod' import { create } from 'zustand' +const resolvedThemeSchema = z.enum(['light', 'dark']) const themeModeSchema = z.enum(['light', 'dark', 'auto']) -const prefersModeSchema = z.enum(['light', 'dark']) +const themeKey = 'theme' type ThemeMode = z.infer -type PrefersMode = z.infer +type ResolvedTheme = z.infer interface ThemeStore { mode: ThemeMode - prefers: PrefersMode - toggleMode: () => void - setPrefers: (prefers: PrefersMode) => void + toggleThemeMode: () => void } -const updateThemeCookie = createServerFn({ method: 'POST' }) - .validator(themeModeSchema) - .handler((ctx) => { - setCookie('theme', ctx.data, { - httpOnly: false, - sameSite: 'lax', - secure: process.env.NODE_ENV === 'production', - path: '/', - maxAge: 60 * 60 * 24 * 365 * 10, - }) - }) +function getStoredThemeMode(): ThemeMode { + if (typeof window === 'undefined') return 'auto' + try { + const storedTheme = localStorage.getItem(themeKey) + return resolvedThemeSchema.parse(storedTheme) + } catch { + return 'auto' + } +} -export const getThemeCookie = createServerFn().handler(() => { - return ( - themeModeSchema.catch('auto').parse(getCookie('theme') ?? 'null') || 'auto' - ) -}) - -export const useThemeStore = create((set, get) => ({ - mode: 'auto', - prefers: (() => { - if (typeof document !== 'undefined') { - return window.matchMedia('(prefers-color-scheme: dark)').matches - ? 'dark' - : 'light' - } - - return 'light' - })(), - toggleMode: () => +function setStoredThemeMode(theme: ThemeMode): void { + if (typeof window === 'undefined') return + try { + const parsedTheme = themeModeSchema.parse(theme) + localStorage.setItem(themeKey, parsedTheme) + } catch {} +} + +function getSystemTheme(): ResolvedTheme { + if (typeof window === 'undefined') return 'light' + return window.matchMedia('(prefers-color-scheme: dark)').matches + ? 'dark' + : 'light' +} + +function updateThemeClass(themeMode: ThemeMode) { + const root = document.documentElement + root.classList.remove('light', 'dark', 'auto') + const newTheme = themeMode === 'auto' ? getSystemTheme() : themeMode + root.classList.add(newTheme) + + if (themeMode === 'auto') { + root.classList.add('auto') + } +} + +export const useThemeStore = create((set) => ({ + mode: getStoredThemeMode(), + toggleThemeMode: () => set((s) => { const newMode = s.mode === 'auto' ? 'light' : s.mode === 'light' ? 'dark' : 'auto' - updateThemeClass(newMode, s.prefers) - updateThemeCookie({ - data: newMode, - }) + updateThemeClass(newMode) + setStoredThemeMode(newMode) - return { - mode: newMode, - } + return { mode: newMode } }), - setPrefers: (prefers) => { - set({ prefers }) - updateThemeClass(get().mode, prefers) - }, })) if (typeof document !== 'undefined') { window .matchMedia('(prefers-color-scheme: dark)') - .addEventListener('change', (event) => { + .addEventListener('change', () => { if (useThemeStore.getState().mode === 'auto') { + updateThemeClass('auto') } - useThemeStore.getState().setPrefers(event.matches ? 'dark' : 'light') }) } -// Helper to update class -function updateThemeClass(mode: ThemeMode, prefers: PrefersMode) { - document.documentElement.classList.remove('dark') - if (mode === 'dark' || (mode === 'auto' && prefers === 'dark')) { - document.documentElement.classList.add('dark') - } -} - export function ThemeToggle() { - const mode = useThemeStore((s) => s.mode) - const toggleMode = useThemeStore((s) => s.toggleMode) + const toggleMode = useThemeStore((s) => s.toggleThemeMode) const handleToggleMode = ( e: React.MouseEvent @@ -108,35 +97,55 @@ export function ThemeToggle() { >
Auto
) } + +export function ThemeDetector() { + return ( + + ) +} diff --git a/src/routes/__root.tsx b/src/routes/__root.tsx index ff4664b87..81a4b510d 100644 --- a/src/routes/__root.tsx +++ b/src/routes/__root.tsx @@ -2,7 +2,6 @@ import * as React from 'react' import * as ReactDom from 'react-dom' import { Outlet, - ScriptOnce, createRootRouteWithContext, redirect, useMatches, @@ -20,11 +19,11 @@ import { TanStackRouterDevtoolsInProd } from '@tanstack/react-router-devtools' import { NotFound } from '~/components/NotFound' import { CgSpinner } from 'react-icons/cg' import { DefaultCatchBoundary } from '~/components/DefaultCatchBoundary' -import { getThemeCookie, useThemeStore } from '~/components/ThemeToggle' import { GamScripts } from '~/components/Gam' import { BackgroundAnimation } from '~/components/BackgroundAnimation' import { SearchProvider } from '~/contexts/SearchContext' import { SearchModal } from '~/components/SearchModal' +import { ThemeDetector } from '~/components/ThemeToggle' export const Route = createRootRouteWithContext<{ queryClient: QueryClient @@ -127,11 +126,6 @@ export const Route = createRootRouteWithContext<{ } }, staleTime: Infinity, - loader: async () => { - return { - themeCookie: await getThemeCookie(), - } - }, errorComponent: (props) => { return ( @@ -169,13 +163,6 @@ function RootComponent() { } function RootDocument({ children }: { children: React.ReactNode }) { - const { themeCookie } = Route.useLoaderData() - - React.useEffect(() => { - useThemeStore.setState({ mode: themeCookie }) - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []) - const matches = useMatches() const isLoading = useRouterState({ @@ -200,17 +187,10 @@ function RootDocument({ children }: { children: React.ReactNode }) { const showDevtools = canShowLoading && isRouterPage - const themeClass = themeCookie === 'dark' ? 'dark' : '' - return ( - + - {/* If the theme is set to auto, inject a tiny script to set the proper class on html based on the user preference */} - {themeCookie === 'auto' ? ( - - ) : null} + {matches.find((d) => d.staticData?.baseParent) ? ( diff --git a/src/routes/_libraries/route.tsx b/src/routes/_libraries/route.tsx index b096e6781..f7bbd42c8 100644 --- a/src/routes/_libraries/route.tsx +++ b/src/routes/_libraries/route.tsx @@ -18,7 +18,7 @@ import { import { getSponsorsForSponsorPack } from '~/server/sponsors' import { libraries } from '~/libraries' import { Scarf } from '~/components/Scarf' -import { ThemeToggle, useThemeStore } from '~/components/ThemeToggle' +import { ThemeToggle } from '~/components/ThemeToggle' import { TbBrandBluesky, TbBrandTwitter } from 'react-icons/tb' import { BiSolidCheckShield } from 'react-icons/bi' import { SearchButton } from '~/components/SearchButton' diff --git a/tailwind.config.cjs b/tailwind.config.cjs index 821368431..d8250fa23 100644 --- a/tailwind.config.cjs +++ b/tailwind.config.cjs @@ -1,7 +1,14 @@ /** @type {import('tailwindcss').Config} */ module.exports = { content: ['./src/**/*.{js,ts,jsx,tsx}'], - plugins: [require('@tailwindcss/typography')], + plugins: [ + require('@tailwindcss/typography'), + function ({ addVariant }) { + addVariant('light', '&:is(.light *)') + addVariant('dark', '&:is(.dark *)') + addVariant('auto', '&:is(.auto *)') + }, + ], darkMode: 'class', theme: { extend: { From b1ba62d29b51057c903bfbe9a4a30180a6d03cb5 Mon Sep 17 00:00:00 2001 From: Leonardo Montini Date: Wed, 6 Aug 2025 18:51:28 +0200 Subject: [PATCH 2/7] refactor: use react context instead of zustand --- src/components/ThemeProvider.tsx | 132 +++++++++++++++++++++++++++++++ src/components/ThemeToggle.tsx | 110 +------------------------- src/routes/__root.tsx | 16 ++-- 3 files changed, 142 insertions(+), 116 deletions(-) create mode 100644 src/components/ThemeProvider.tsx diff --git a/src/components/ThemeProvider.tsx b/src/components/ThemeProvider.tsx new file mode 100644 index 000000000..895140af3 --- /dev/null +++ b/src/components/ThemeProvider.tsx @@ -0,0 +1,132 @@ +import { ScriptOnce } from '@tanstack/react-router' +import * as React from 'react' +import { createContext, ReactNode, useEffect, useState } from 'react' +import { z } from 'zod' + +const themeModeSchema = z.enum(['light', 'dark', 'auto']) +const resolvedThemeSchema = z.enum(['light', 'dark']) +const themeKey = 'theme' + +type ThemeMode = z.infer +type ResolvedTheme = z.infer + +function getStoredThemeMode(): ThemeMode { + if (typeof window === 'undefined') return 'auto' + try { + const storedTheme = localStorage.getItem(themeKey) + return themeModeSchema.parse(storedTheme) + } catch { + return 'auto' + } +} + +function setStoredThemeMode(theme: ThemeMode): void { + if (typeof window === 'undefined') return + try { + const parsedTheme = themeModeSchema.parse(theme) + localStorage.setItem(themeKey, parsedTheme) + } catch {} +} + +function getSystemTheme(): ResolvedTheme { + if (typeof window === 'undefined') return 'light' + return window.matchMedia('(prefers-color-scheme: dark)').matches + ? 'dark' + : 'light' +} + +function updateThemeClass(themeMode: ThemeMode) { + const root = document.documentElement + root.classList.remove('light', 'dark', 'auto') + const newTheme = themeMode === 'auto' ? getSystemTheme() : themeMode + root.classList.add(newTheme) + + if (themeMode === 'auto') { + root.classList.add('auto') + } +} + +function setupPreferredListener() { + const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)') + const handler = () => updateThemeClass('auto') + mediaQuery.addEventListener('change', handler) + return () => mediaQuery.removeEventListener('change', handler) +} + +const themeDetectorScript = (function () { + function themeFn() { + try { + const storedTheme = localStorage.getItem('theme') || 'auto' + + if (storedTheme === 'auto') { + const autoTheme = window.matchMedia('(prefers-color-scheme: dark)') + .matches + ? 'dark' + : 'light' + document.documentElement.classList.add(autoTheme, 'auto') + } else { + document.documentElement.classList.add(storedTheme) + } + } catch (e) { + const autoTheme = window.matchMedia('(prefers-color-scheme: dark)') + .matches + ? 'dark' + : 'light' + document.documentElement.classList.add(autoTheme, 'auto') + } + } + return `(${themeFn.toString()})();` +})() + +type ThemeContextProps = { + themeMode: ThemeMode + resolvedTheme: ResolvedTheme + setTheme: (theme: ThemeMode) => void + toggleMode: () => void +} +const ThemeContext = createContext(undefined) + +type ThemeProviderProps = { + children: ReactNode +} +export function ThemeProvider({ children }: ThemeProviderProps) { + const [themeMode, setThemeMode] = useState(getStoredThemeMode) + + useEffect(() => { + updateThemeClass(themeMode) + + if (themeMode === 'auto') { + return setupPreferredListener() + } + }, [themeMode]) + + const resolvedTheme = themeMode === 'auto' ? getSystemTheme() : themeMode + + const setTheme = (newTheme: ThemeMode) => { + setThemeMode(newTheme) + setStoredThemeMode(newTheme) + } + + const toggleMode = () => { + setTheme( + themeMode === 'light' ? 'dark' : themeMode === 'dark' ? 'auto' : 'light' + ) + } + + return ( + + + {children} + + ) +} + +export const useTheme = () => { + const context = React.useContext(ThemeContext) + if (!context) { + throw new Error('useTheme must be used within a ThemeProvider') + } + return context +} diff --git a/src/components/ThemeToggle.tsx b/src/components/ThemeToggle.tsx index 2b08e32bb..f807ace04 100644 --- a/src/components/ThemeToggle.tsx +++ b/src/components/ThemeToggle.tsx @@ -1,84 +1,10 @@ -import { ScriptOnce } from '@tanstack/react-router' import * as React from 'react' import { FaMoon, FaSun } from 'react-icons/fa' +import { useTheme } from './ThemeProvider' import { twMerge } from 'tailwind-merge' -import { z } from 'zod' -import { create } from 'zustand' - -const resolvedThemeSchema = z.enum(['light', 'dark']) -const themeModeSchema = z.enum(['light', 'dark', 'auto']) -const themeKey = 'theme' - -type ThemeMode = z.infer -type ResolvedTheme = z.infer - -interface ThemeStore { - mode: ThemeMode - toggleThemeMode: () => void -} - -function getStoredThemeMode(): ThemeMode { - if (typeof window === 'undefined') return 'auto' - try { - const storedTheme = localStorage.getItem(themeKey) - return resolvedThemeSchema.parse(storedTheme) - } catch { - return 'auto' - } -} - -function setStoredThemeMode(theme: ThemeMode): void { - if (typeof window === 'undefined') return - try { - const parsedTheme = themeModeSchema.parse(theme) - localStorage.setItem(themeKey, parsedTheme) - } catch {} -} - -function getSystemTheme(): ResolvedTheme { - if (typeof window === 'undefined') return 'light' - return window.matchMedia('(prefers-color-scheme: dark)').matches - ? 'dark' - : 'light' -} - -function updateThemeClass(themeMode: ThemeMode) { - const root = document.documentElement - root.classList.remove('light', 'dark', 'auto') - const newTheme = themeMode === 'auto' ? getSystemTheme() : themeMode - root.classList.add(newTheme) - - if (themeMode === 'auto') { - root.classList.add('auto') - } -} - -export const useThemeStore = create((set) => ({ - mode: getStoredThemeMode(), - toggleThemeMode: () => - set((s) => { - const newMode = - s.mode === 'auto' ? 'light' : s.mode === 'light' ? 'dark' : 'auto' - - updateThemeClass(newMode) - setStoredThemeMode(newMode) - - return { mode: newMode } - }), -})) - -if (typeof document !== 'undefined') { - window - .matchMedia('(prefers-color-scheme: dark)') - .addEventListener('change', () => { - if (useThemeStore.getState().mode === 'auto') { - updateThemeClass('auto') - } - }) -} export function ThemeToggle() { - const toggleMode = useThemeStore((s) => s.toggleThemeMode) + const { toggleMode } = useTheme() const handleToggleMode = ( e: React.MouseEvent @@ -117,35 +43,3 @@ export function ThemeToggle() { ) } - -export function ThemeDetector() { - return ( - - ) -} diff --git a/src/routes/__root.tsx b/src/routes/__root.tsx index 81a4b510d..4b7d23596 100644 --- a/src/routes/__root.tsx +++ b/src/routes/__root.tsx @@ -1,5 +1,4 @@ import * as React from 'react' -import * as ReactDom from 'react-dom' import { Outlet, createRootRouteWithContext, @@ -23,7 +22,7 @@ import { GamScripts } from '~/components/Gam' import { BackgroundAnimation } from '~/components/BackgroundAnimation' import { SearchProvider } from '~/contexts/SearchContext' import { SearchModal } from '~/components/SearchModal' -import { ThemeDetector } from '~/components/ThemeToggle' +import { ThemeProvider } from '~/components/ThemeProvider' export const Route = createRootRouteWithContext<{ queryClient: QueryClient @@ -153,11 +152,13 @@ function RootComponent() { return ( - - - - - + + + + + + + ) } @@ -190,7 +191,6 @@ function RootDocument({ children }: { children: React.ReactNode }) { return ( - {matches.find((d) => d.staticData?.baseParent) ? ( From 29b36d6a79ad9cb5aca938ef1a1381b3e15acc61 Mon Sep 17 00:00:00 2001 From: Leonardo Montini Date: Wed, 6 Aug 2025 23:33:34 +0200 Subject: [PATCH 3/7] refactor: use isomorphic and clientOnly functions --- src/components/ThemeProvider.tsx | 54 ++++++++++++++++---------------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/src/components/ThemeProvider.tsx b/src/components/ThemeProvider.tsx index 895140af3..0b011bab8 100644 --- a/src/components/ThemeProvider.tsx +++ b/src/components/ThemeProvider.tsx @@ -1,4 +1,5 @@ import { ScriptOnce } from '@tanstack/react-router' +import { clientOnly, createIsomorphicFn } from '@tanstack/react-start' import * as React from 'react' import { createContext, ReactNode, useEffect, useState } from 'react' import { z } from 'zod' @@ -10,32 +11,33 @@ const themeKey = 'theme' type ThemeMode = z.infer type ResolvedTheme = z.infer -function getStoredThemeMode(): ThemeMode { - if (typeof window === 'undefined') return 'auto' - try { - const storedTheme = localStorage.getItem(themeKey) - return themeModeSchema.parse(storedTheme) - } catch { - return 'auto' - } -} +const getStoredThemeMode = createIsomorphicFn() + .server((): ThemeMode => 'auto') + .client((): ThemeMode => { + try { + const storedTheme = localStorage.getItem(themeKey) + return themeModeSchema.parse(storedTheme) + } catch { + return 'auto' + } + }) -function setStoredThemeMode(theme: ThemeMode): void { - if (typeof window === 'undefined') return +const setStoredThemeMode = clientOnly((theme: ThemeMode) => { try { const parsedTheme = themeModeSchema.parse(theme) localStorage.setItem(themeKey, parsedTheme) } catch {} -} +}) -function getSystemTheme(): ResolvedTheme { - if (typeof window === 'undefined') return 'light' - return window.matchMedia('(prefers-color-scheme: dark)').matches - ? 'dark' - : 'light' -} +const getSystemTheme = createIsomorphicFn() + .server((): ResolvedTheme => 'light') + .client((): ResolvedTheme => { + return window.matchMedia('(prefers-color-scheme: dark)').matches + ? 'dark' + : 'light' + }) -function updateThemeClass(themeMode: ThemeMode) { +const updateThemeClass = clientOnly((themeMode: ThemeMode) => { const root = document.documentElement root.classList.remove('light', 'dark', 'auto') const newTheme = themeMode === 'auto' ? getSystemTheme() : themeMode @@ -44,14 +46,14 @@ function updateThemeClass(themeMode: ThemeMode) { if (themeMode === 'auto') { root.classList.add('auto') } -} +}) -function setupPreferredListener() { +const setupPreferredListener = clientOnly(() => { const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)') const handler = () => updateThemeClass('auto') mediaQuery.addEventListener('change', handler) return () => mediaQuery.removeEventListener('change', handler) -} +}) const themeDetectorScript = (function () { function themeFn() { @@ -93,11 +95,8 @@ export function ThemeProvider({ children }: ThemeProviderProps) { const [themeMode, setThemeMode] = useState(getStoredThemeMode) useEffect(() => { - updateThemeClass(themeMode) - - if (themeMode === 'auto') { - return setupPreferredListener() - } + if (themeMode !== 'auto') return + return setupPreferredListener() }, [themeMode]) const resolvedTheme = themeMode === 'auto' ? getSystemTheme() : themeMode @@ -105,6 +104,7 @@ export function ThemeProvider({ children }: ThemeProviderProps) { const setTheme = (newTheme: ThemeMode) => { setThemeMode(newTheme) setStoredThemeMode(newTheme) + updateThemeClass(newTheme) } const toggleMode = () => { From 06085db9cd102ed073bb303f7413c6aee5dbee47 Mon Sep 17 00:00:00 2001 From: Leonardo Montini Date: Thu, 7 Aug 2025 08:42:18 +0200 Subject: [PATCH 4/7] fix: handle unexpected value from localStorage --- src/components/ThemeProvider.tsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/components/ThemeProvider.tsx b/src/components/ThemeProvider.tsx index 0b011bab8..5d46def0a 100644 --- a/src/components/ThemeProvider.tsx +++ b/src/components/ThemeProvider.tsx @@ -59,15 +59,18 @@ const themeDetectorScript = (function () { function themeFn() { try { const storedTheme = localStorage.getItem('theme') || 'auto' + const validTheme = ['light', 'dark', 'auto'].includes(storedTheme) + ? storedTheme + : 'auto' - if (storedTheme === 'auto') { + if (validTheme === 'auto') { const autoTheme = window.matchMedia('(prefers-color-scheme: dark)') .matches ? 'dark' : 'light' document.documentElement.classList.add(autoTheme, 'auto') } else { - document.documentElement.classList.add(storedTheme) + document.documentElement.classList.add(validTheme) } } catch (e) { const autoTheme = window.matchMedia('(prefers-color-scheme: dark)') From 483bcb263a9168cc592cb57bf3e758b34e9ad486 Mon Sep 17 00:00:00 2001 From: Leonardo Montini Date: Thu, 7 Aug 2025 09:07:52 +0200 Subject: [PATCH 5/7] fix: ThemeToggle shows light if no theme class is set --- src/components/ThemeToggle.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/ThemeToggle.tsx b/src/components/ThemeToggle.tsx index f807ace04..2d005c3d7 100644 --- a/src/components/ThemeToggle.tsx +++ b/src/components/ThemeToggle.tsx @@ -37,7 +37,7 @@ export function ThemeToggle() {
From 744f2ed6e2f2c1540dc49779993a1c4d4e2bbc0d Mon Sep 17 00:00:00 2001 From: Leonardo Montini Date: Thu, 7 Aug 2025 09:55:20 +0200 Subject: [PATCH 6/7] feat: after auto goes opposite to system --- src/components/ThemeProvider.tsx | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/components/ThemeProvider.tsx b/src/components/ThemeProvider.tsx index 5d46def0a..30f12024b 100644 --- a/src/components/ThemeProvider.tsx +++ b/src/components/ThemeProvider.tsx @@ -55,6 +55,14 @@ const setupPreferredListener = clientOnly(() => { return () => mediaQuery.removeEventListener('change', handler) }) +const getNextTheme = clientOnly((current: ThemeMode): ThemeMode => { + const themes: ThemeMode[] = + getSystemTheme() === 'dark' + ? ['auto', 'light', 'dark'] + : ['auto', 'dark', 'light'] + return themes[(themes.indexOf(current) + 1) % themes.length] +}) + const themeDetectorScript = (function () { function themeFn() { try { @@ -111,9 +119,7 @@ export function ThemeProvider({ children }: ThemeProviderProps) { } const toggleMode = () => { - setTheme( - themeMode === 'light' ? 'dark' : themeMode === 'dark' ? 'auto' : 'light' - ) + setTheme(getNextTheme(themeMode)) } return ( From e6ffe3caef3bdea81a1e0334281c8a7f328da529 Mon Sep 17 00:00:00 2001 From: Leonardo Montini Date: Thu, 7 Aug 2025 10:16:41 +0200 Subject: [PATCH 7/7] fix: janky knob animation --- src/components/ThemeToggle.tsx | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/components/ThemeToggle.tsx b/src/components/ThemeToggle.tsx index 2d005c3d7..20c6e5c60 100644 --- a/src/components/ThemeToggle.tsx +++ b/src/components/ThemeToggle.tsx @@ -1,7 +1,6 @@ import * as React from 'react' import { FaMoon, FaSun } from 'react-icons/fa' import { useTheme } from './ThemeProvider' -import { twMerge } from 'tailwind-merge' export function ThemeToggle() { const { toggleMode } = useTheme() @@ -17,9 +16,7 @@ export function ThemeToggle() { return (