From e6f4e3b2e56521923fabaa1dc2370b2cde129675 Mon Sep 17 00:00:00 2001 From: yiekheng Date: Sun, 10 May 2026 09:04:27 +0800 Subject: [PATCH] =?UTF-8?q?revert(web):=20restore=20theme=20toggle=20?= =?UTF-8?q?=E2=80=94=20gcr=20extension,=20not=20next-themes,=20was=20the?= =?UTF-8?q?=20issue?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit I incorrectly removed next-themes thinking it caused the hydration warning. The actual mismatch was a `__gcrremoteframetoken` attribute added to by a browser extension, which the previous commit already addressed via `suppressHydrationWarning`. Restored: - ThemeProvider wrap in the layout - ThemeToggle component - Sonner Toaster's useTheme() so toasts respect the chosen theme - Appearance card on the Settings page Co-Authored-By: Claude Opus 4.7 (1M context) --- apps/web/src/app/layout.tsx | 19 ++++++---- apps/web/src/app/settings/page.tsx | 11 ++++++ apps/web/src/components/theme-provider.tsx | 20 +++++++++++ apps/web/src/components/theme-toggle.tsx | 42 ++++++++++++++++++++++ apps/web/src/components/ui/sonner.tsx | 5 ++- 5 files changed, 89 insertions(+), 8 deletions(-) create mode 100644 apps/web/src/components/theme-provider.tsx create mode 100644 apps/web/src/components/theme-toggle.tsx diff --git a/apps/web/src/app/layout.tsx b/apps/web/src/app/layout.tsx index 3e1854c..171c7ea 100644 --- a/apps/web/src/app/layout.tsx +++ b/apps/web/src/app/layout.tsx @@ -1,5 +1,6 @@ import type { Metadata, Viewport } from "next"; import { GeistSans } from "geist/font/sans"; +import { ThemeProvider } from "@/components/theme-provider"; import { AppShell } from "@/components/app-shell"; import { Toaster } from "@/components/ui/sonner"; import "./globals.css"; @@ -21,15 +22,19 @@ export const viewport: Viewport = { export default function RootLayout({ children }: { children: React.ReactNode }) { return ( // `suppressHydrationWarning` here is for *attribute* differences only. - // Browser extensions (password managers, accessibility tools, the - // Google `__gcrremoteframetoken` injector, etc.) commonly add data-/ - // dunder attributes to the element after the document loads, - // which React 19 otherwise flags as a hydration mismatch. Children - // are still hydration-checked normally. + // Two sources legitimately mutate / attributes after the + // document loads: + // - next-themes adds the `class="light|dark"` (and the colour-scheme + // style) before React hydrates, + // - browser extensions inject dunder attributes like + // `__gcrremoteframetoken`, password-manager flags, etc. + // Children are still hydration-checked normally so real bugs surface. - {children} - + + {children} + + ); diff --git a/apps/web/src/app/settings/page.tsx b/apps/web/src/app/settings/page.tsx index 6cb4d6f..871aeaa 100644 --- a/apps/web/src/app/settings/page.tsx +++ b/apps/web/src/app/settings/page.tsx @@ -1,6 +1,7 @@ import { getSeededOperator } from "@/lib/operator"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Separator } from "@/components/ui/separator"; +import { ThemeToggle } from "@/components/theme-toggle"; export default async function SettingsPage() { const op = await getSeededOperator(); @@ -23,6 +24,16 @@ export default async function SettingsPage() { + + + Appearance + + +
Theme
+ +
+
+

cm WhatsApp Bot ยท self-hosted

diff --git a/apps/web/src/components/theme-provider.tsx b/apps/web/src/components/theme-provider.tsx new file mode 100644 index 0000000..fcb9b9d --- /dev/null +++ b/apps/web/src/components/theme-provider.tsx @@ -0,0 +1,20 @@ +"use client"; + +import { ThemeProvider as NextThemesProvider } from "next-themes"; +import type { ComponentProps } from "react"; + +type ThemeProviderProps = ComponentProps; + +export function ThemeProvider({ children, ...props }: ThemeProviderProps) { + return ( + + {children} + + ); +} diff --git a/apps/web/src/components/theme-toggle.tsx b/apps/web/src/components/theme-toggle.tsx new file mode 100644 index 0000000..b7b0927 --- /dev/null +++ b/apps/web/src/components/theme-toggle.tsx @@ -0,0 +1,42 @@ +"use client"; + +import { useTheme } from "next-themes"; +import { Moon, Sun, Monitor } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; + +export function ThemeToggle() { + const { setTheme, theme } = useTheme(); + return ( + + + + + + setTheme("light")}> + Light + + setTheme("dark")}> + Dark + + setTheme("system")}> + System + + + + ); +} diff --git a/apps/web/src/components/ui/sonner.tsx b/apps/web/src/components/ui/sonner.tsx index 994436c..9280ee5 100644 --- a/apps/web/src/components/ui/sonner.tsx +++ b/apps/web/src/components/ui/sonner.tsx @@ -1,12 +1,15 @@ "use client" +import { useTheme } from "next-themes" import { Toaster as Sonner, type ToasterProps } from "sonner" import { CircleCheckIcon, InfoIcon, TriangleAlertIcon, OctagonXIcon, Loader2Icon } from "lucide-react" const Toaster = ({ ...props }: ToasterProps) => { + const { theme = "system" } = useTheme() + return (