diff --git a/bun.lockb b/bun.lockb index 5536a146c..07e157fee 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/package.json b/package.json index a4412336d..edcb361ce 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,8 @@ "jsontoxml": "^1.0.1", "katex": "^0.16.9", "memoizee": "^0.4.15", - "next": "^14.0.4", + "next": "^14.1.0", + "next-themes": "^0.2.1", "object-hash": "^3.0.0", "p-map": "^7.0.0", "parse-cache-control": "^1.0.1", diff --git a/src/app/[spaceId]/ClientContexts.tsx b/src/app/[spaceId]/ClientContexts.tsx index f2b1ce7da..11870d518 100644 --- a/src/app/[spaceId]/ClientContexts.tsx +++ b/src/app/[spaceId]/ClientContexts.tsx @@ -1,5 +1,7 @@ 'use client'; +import { CustomizationThemeMode } from '@gitbook/api'; +import { ThemeProvider } from 'next-themes'; import React from 'react'; import { RecoilRoot } from 'recoil'; @@ -7,14 +9,18 @@ import { TranslateContext } from '@/intl/client'; import { TranslationLanguage } from '@/intl/translations'; export function ClientContexts(props: { + nonce: string; language: TranslationLanguage; + forcedTheme: CustomizationThemeMode | undefined; children: React.ReactNode; }) { - const { children, language } = props; + const { nonce, children, forcedTheme, language } = props; return ( - - {children} - + + + {children} + + ); } diff --git a/src/app/[spaceId]/layout.tsx b/src/app/[spaceId]/layout.tsx index ea30ac13c..ab3f59f59 100644 --- a/src/app/[spaceId]/layout.tsx +++ b/src/app/[spaceId]/layout.tsx @@ -6,6 +6,7 @@ import { fonts, ibmPlexMono } from '@/fonts'; import { getSpaceLanguage } from '@/intl/server'; import { getSpaceContent } from '@/lib/api'; import { hexToRgb, shadesOfColor } from '@/lib/colors'; +import { getContentSecurityPolicyNonce } from '@/lib/csp'; import { tcls } from '@/lib/tailwind'; import { ClientContexts } from './ClientContexts'; @@ -27,9 +28,11 @@ export default async function SpaceRootLayout(props: { }); const headerTheme = generateHeaderTheme(customization); const language = getSpaceLanguage(customization); + const nonce = getContentSecurityPolicyNonce(); return ( - {children} + + {children} + ); diff --git a/src/components/ThemeToggler/ThemeToggler.tsx b/src/components/ThemeToggler/ThemeToggler.tsx index 1e1aa6d5b..bafb473e7 100644 --- a/src/components/ThemeToggler/ThemeToggler.tsx +++ b/src/components/ThemeToggler/ThemeToggler.tsx @@ -3,6 +3,7 @@ import Monitor from '@geist-ui/icons/monitor'; import Moon from '@geist-ui/icons/moon'; import Sun from '@geist-ui/icons/sun'; +import { useTheme } from 'next-themes'; import React from 'react'; import { IconComponent } from '@/components/icons'; @@ -11,42 +12,22 @@ import { tcls } from '@/lib/tailwind'; type ThemeMode = 'light' | 'system' | 'dark'; -const LOCALSTORAGE_KEY = 'color-theme'; - /** * Buttons to toggle between light/system/dark modes. */ export function ThemeToggler(props: {}) { - const [mode, setMode] = React.useState('system'); - const language = useLanguage(); - const onSwitchMode = (to: ThemeMode) => { - setMode(to); - window.localStorage.setItem(LOCALSTORAGE_KEY, to); - }; + const [mounted, setMounted] = React.useState(false); + const { theme, setTheme } = useTheme(); React.useEffect(() => { - const value = window.localStorage.getItem(LOCALSTORAGE_KEY); - if (value && (value === 'light' || value === 'system' || value === 'dark')) { - setMode(value); - } + setMounted(true); }, []); - React.useEffect(() => { - const applied = - mode === 'system' - ? window.matchMedia('(prefers-color-scheme: dark)').matches - ? 'dark' - : 'light' - : mode; - - if (applied === 'light') { - document.documentElement.classList.remove('dark'); - } else { - document.documentElement.classList.add('dark'); - } - }, [mode]); + const onSwitchMode = (to: ThemeMode) => { + setTheme(to); + }; return (
onSwitchMode('light')} title={tString(language, 'switch_to_light_theme')} /> onSwitchMode('system')} title={tString(language, 'switch_to_system_theme')} /> onSwitchMode('dark')} title={tString(language, 'switch_to_dark_theme')}