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 {