diff --git a/apps/web/src/components/providers.tsx b/apps/web/src/components/providers.tsx index 6b43f0c2..1bdb48af 100644 --- a/apps/web/src/components/providers.tsx +++ b/apps/web/src/components/providers.tsx @@ -1,9 +1,36 @@ "use client"; +import { useHotkey } from "@tanstack/react-hotkeys"; import { RootProvider } from "fumadocs-ui/provider/next"; import type { ReactNode } from "react"; import { Toaster } from "sonner"; +import { useThemeTransition } from "@/components/use-theme-transition"; + +function ThemeHotkey() { + const { toggleTheme } = useThemeTransition(); + + useHotkey( + "D", + (event) => { + if ( + event.defaultPrevented || + event.isComposing || + event.keyCode === 229 || + (event.target instanceof HTMLElement && event.target.closest('[role="dialog"]')) + ) { + return; + } + + event.preventDefault(); + toggleTheme(); + }, + { ignoreInputs: true, preventDefault: false, requireReset: true, stopPropagation: false }, + ); + + return null; +} + export function Providers({ children }: { children: ReactNode }) { return ( + {children} diff --git a/apps/web/src/components/use-theme-transition.ts b/apps/web/src/components/use-theme-transition.ts index 876ab8ae..7ca07c72 100644 --- a/apps/web/src/components/use-theme-transition.ts +++ b/apps/web/src/components/use-theme-transition.ts @@ -7,21 +7,10 @@ import { flushSync } from "react-dom"; type ActiveTheme = "dark" | "light"; type ThemeMode = ActiveTheme | "system"; -type ViewTransitionLike = { - finished: Promise; -}; - type DocumentWithViewTransition = Document & { - startViewTransition?: (updateCallback: () => Promise | void) => ViewTransitionLike; + startViewTransition?: (updateCallback: () => void) => void; }; -function applyThemeToDocument(theme: ActiveTheme) { - const root = document.documentElement; - root.classList.remove("light", "dark"); - root.classList.add(theme); - root.style.colorScheme = theme; -} - function getSystemTheme(systemTheme?: string): ActiveTheme { if (systemTheme === "dark" || systemTheme === "light") { return systemTheme; @@ -33,7 +22,6 @@ function getSystemTheme(systemTheme?: string): ActiveTheme { export function useThemeTransition() { const { resolvedTheme, setTheme, systemTheme, theme } = useTheme(); const [mounted, setMounted] = useState(false); - const [isTransitioning, setIsTransitioning] = useState(false); useEffect(() => { setMounted(true); @@ -51,43 +39,24 @@ export function useThemeTransition() { : `Switch to ${nextAppliedTheme} theme`; const toggleTheme = () => { - if (!mounted || isTransitioning) { - return; - } - - if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) { - setTheme(nextMode); - return; - } + if (!mounted) return; const documentWithTransition = document as DocumentWithViewTransition; - - if (!documentWithTransition.startViewTransition) { + if ( + window.matchMedia("(prefers-reduced-motion: reduce)").matches || + !documentWithTransition.startViewTransition + ) { setTheme(nextMode); return; } - const runTransition = async () => { - setIsTransitioning(true); - document.documentElement.dataset.themeTransition = "active"; - - try { - const transition = documentWithTransition.startViewTransition(() => { - applyThemeToDocument(nextAppliedTheme); - - flushSync(() => { - setTheme(nextMode); - }); - }); - - await transition.finished; - } finally { - delete document.documentElement.dataset.themeTransition; - setIsTransitioning(false); - } - }; - - void runTransition(); + documentWithTransition.startViewTransition(() => { + const root = document.documentElement; + root.classList.remove("light", "dark"); + root.classList.add(nextAppliedTheme); + root.style.colorScheme = nextAppliedTheme; + flushSync(() => setTheme(nextMode)); + }); }; return { diff --git a/apps/web/src/styles/globals.css b/apps/web/src/styles/globals.css index 21d72d62..a8de0841 100644 --- a/apps/web/src/styles/globals.css +++ b/apps/web/src/styles/globals.css @@ -61,15 +61,6 @@ } } -@keyframes theme-line-reveal { - from { - clip-path: inset(0 0 100% 0); - } - to { - clip-path: inset(0 0 0 0); - } -} - .animate-logo-marquee { animation: logo-marquee 25s linear infinite !important; animation-iteration-count: infinite !important; @@ -140,7 +131,6 @@ --shadow-2xl: 0px 1px 2px 0px hsl(0 0% 0% / 0.45); --tracking-normal: 0em; --spacing: 0.25rem; - --theme-transition-line: oklch(0.145 0 0 / 0.16); } .dark { @@ -179,25 +169,6 @@ --selection: #4f4f4f; --selection-foreground: #ffffff; --path: oklch(0.269 0 0); - --theme-transition-line: oklch(0.985 0 0 / 0.22); -} - -@supports (view-transition-name: root) { - html[data-theme-transition="active"]::view-transition-old(root), - html[data-theme-transition="active"]::view-transition-new(root) { - animation: none; - mix-blend-mode: normal; - } - - html[data-theme-transition="active"]::view-transition-old(root) { - z-index: 1; - } - - html[data-theme-transition="active"]::view-transition-new(root) { - z-index: 2; - animation: theme-line-reveal 720ms cubic-bezier(0.72, 0, 0.2, 1) both; - box-shadow: 0 1px 0 var(--theme-transition-line); - } } @theme inline {