feat: implement dark mode with next-themes and ThemeProvider

This commit is contained in:
ngc2207 2025-01-29 01:05:54 +08:00
parent c85de533e1
commit f139be1c97
5 changed files with 101 additions and 7 deletions

BIN
bun.lockb

Binary file not shown.

View File

@ -13,6 +13,7 @@
"clsx": "^2.1.1", "clsx": "^2.1.1",
"lucide-react": "^0.474.0", "lucide-react": "^0.474.0",
"next": "15.1.6", "next": "15.1.6",
"next-themes": "^0.4.4",
"react": "^19.0.0", "react": "^19.0.0",
"react-dom": "^19.0.0", "react-dom": "^19.0.0",
"tailwind-merge": "^2.6.0", "tailwind-merge": "^2.6.0",

View File

@ -1,6 +1,7 @@
import "@/app/globals.css";
import type { Metadata } from "next"; import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google"; import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css"; import { ThemeProvider } from "@/components/theme-provider";
const geistSans = Geist({ const geistSans = Geist({
variable: "--font-geist-sans", variable: "--font-geist-sans",
@ -17,17 +18,24 @@ export const metadata: Metadata = {
description: "Generated by create next app", description: "Generated by create next app",
}; };
export default function RootLayout({ interface RootLayoutProps {
children,
}: Readonly<{
children: React.ReactNode; children: React.ReactNode;
}>) { }
export default function RootLayout({ children }: RootLayoutProps) {
return ( return (
<html lang="en"> <html lang="en" suppressHydrationWarning>
<body <body
className={`${geistSans.variable} ${geistMono.variable} antialiased`} className={`${geistSans.variable} ${geistMono.variable} antialiased`}
>
<ThemeProvider
attribute="class"
defaultTheme="system"
enableSystem
disableTransitionOnChange
> >
{children} {children}
</ThemeProvider>
</body> </body>
</html> </html>
); );

View File

@ -0,0 +1,11 @@
"use client";
import * as React from "react";
import { ThemeProvider as NextThemesProvider } from "next-themes";
export function ThemeProvider({
children,
...props
}: React.ComponentProps<typeof NextThemesProvider>) {
return <NextThemesProvider {...props}>{children}</NextThemesProvider>;
}

View File

@ -0,0 +1,74 @@
"use client";
import Image from "next/image";
import { useTheme } from "next-themes";
import { Check, Minus } from "lucide-react";
import { useEffect, useState } from "react";
import { Skeleton } from "@/components/ui/skeleton";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
const items = [
{ value: "light", label: "Light", image: "/ui-light.png" },
{ value: "dark", label: "Dark", image: "/ui-dark.png" },
{ value: "system", label: "System", image: "/ui-system.png" },
];
export default function ThemeSwitcher() {
const { theme, setTheme } = useTheme();
const [mounted, setMounted] = useState(false);
useEffect(() => {
setMounted(true);
}, []);
if (!mounted) {
return (
<div className="flex gap-3">
{items.map((item) => (
<div key={item.value} className="flex flex-col items-center">
<Skeleton className="h-[70px] w-[88px] rounded-lg" />
<Skeleton className="mt-2 h-4 w-[88px]" />
</div>
))}
</div>
);
}
return (
<RadioGroup className="flex gap-3" defaultValue={theme}>
{items.map((item) => (
<label key={item.value}>
<RadioGroupItem
id={item.value}
value={item.value}
className="peer sr-only after:absolute after:inset-0"
onClick={() => setTheme(item.value)}
/>
<Image
src={item.image}
alt={item.label}
width={88}
height={70}
priority
className="relative cursor-pointer overflow-hidden rounded-lg border border-input shadow-sm shadow-black/5 outline-offset-2 transition-colors peer-[:focus-visible]:outline peer-[:focus-visible]:outline-2 peer-[:focus-visible]:outline-ring/70 peer-data-[disabled]:cursor-not-allowed peer-data-[state=checked]:border-ring peer-data-[state=checked]:bg-accent peer-data-[disabled]:opacity-50"
/>
<span className="group mt-2 flex items-center gap-1 peer-data-[state=unchecked]:text-muted-foreground/70">
<Check
size={16}
strokeWidth={2}
className="peer-data-[state=unchecked]:group-[]:hidden"
aria-hidden="true"
/>
<Minus
size={16}
strokeWidth={2}
className="peer-data-[state=checked]:group-[]:hidden"
aria-hidden="true"
/>
<span className="text-xs font-medium">{item.label}</span>
</span>
</label>
))}
</RadioGroup>
);
}