dark/light toggle

This commit is contained in:
boilerrat 2025-03-09 05:55:32 -04:00
parent 472396055f
commit 33d0923fc7
7 changed files with 169 additions and 3 deletions

22
CLAUDE.md Normal file
View File

@ -0,0 +1,22 @@
# CLAUDE.md - Guidance for AI Assistance
## Build & Workflow Commands
- Development: `npm run dev`
- Build: `npm run build`
- Start production: `npm run start`
- Lint: `npm run lint`
## Code Style Guidelines
- **Components**: Named function exports with "use client" directive where needed
- **TypeScript**: Strict mode, explicit types for props and functions
- **Naming**: PascalCase for components, camelCase for functions/variables
- **Imports**: React → Next.js → external → internal, use path aliases (@/components, @/lib)
- **Styling**: Tailwind CSS with utility classes, use `cn()` for conditional classes
- **UI Components**: Reusable components in components/ui with consistent prop interfaces
- **Formatting**: 2-space indentation, consistent spacing, trailing commas
- **Error Handling**: Use try/catch for async operations, provide meaningful error messages
## Project Structure
- `/src/app`: Next.js App Router pages and layouts
- `/src/components`: Reusable React components, UI components in `/ui`
- `/src/lib`: Utility functions and helpers

View File

@ -1,6 +1,7 @@
import type { Metadata } from 'next'
import { Inter } from 'next/font/google'
import './globals.css'
import { ThemeProvider } from '@/components/ui/theme-provider'
const inter = Inter({ subsets: ['latin'] })
@ -15,9 +16,16 @@ export default function RootLayout({
children: React.ReactNode
}) {
return (
<html lang="en">
<html lang="en" suppressHydrationWarning>
<body className={inter.className}>
{children}
<ThemeProvider
attribute="class"
defaultTheme="system"
enableSystem
disableTransitionOnChange
>
{children}
</ThemeProvider>
</body>
</html>
)

View File

@ -2,6 +2,7 @@
import Link from "next/link"
import { Button } from "@/components/ui/button"
import { ThemeToggle } from "@/components/ui/theme-toggle"
import { motion } from "framer-motion"
function Navbar() {
@ -33,6 +34,7 @@ function Navbar() {
</nav>
<div className="flex items-center gap-4">
<ThemeToggle />
<Button variant="outline" className="hidden md:flex">
Connect Wallet
</Button>

View File

@ -0,0 +1,97 @@
"use client"
import * as React from "react"
import { getSystemTheme, isBrowser, type Theme } from "@/lib/utils"
type ThemeProviderProps = {
children: React.ReactNode
defaultTheme?: Theme
storageKey?: string
attribute?: string
enableSystem?: boolean
disableTransitionOnChange?: boolean
}
type ThemeProviderState = {
theme: Theme
setTheme: (theme: Theme) => void
}
const initialState: ThemeProviderState = {
theme: "system",
setTheme: () => null,
}
export const ThemeProviderContext = React.createContext<ThemeProviderState>(initialState)
export function ThemeProvider({
children,
defaultTheme = "system",
storageKey = "theme",
...props
}: ThemeProviderProps) {
const [theme, setTheme] = React.useState<Theme>(
() => {
if (!isBrowser) return defaultTheme
return (localStorage.getItem(storageKey) as Theme) || defaultTheme
}
)
React.useEffect(() => {
const root = window.document.documentElement
// Remove old class
root.classList.remove("light", "dark")
// Add new class based on theme
if (theme === "system") {
const systemTheme = getSystemTheme()
root.classList.add(systemTheme)
} else {
root.classList.add(theme)
}
}, [theme])
// Monitor for system theme change
React.useEffect(() => {
if (theme !== "system") return
const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)")
const handleChange = () => {
const root = window.document.documentElement
const systemTheme = getSystemTheme()
root.classList.remove("light", "dark")
root.classList.add(systemTheme)
}
mediaQuery.addEventListener("change", handleChange)
return () => mediaQuery.removeEventListener("change", handleChange)
}, [theme])
const value = React.useMemo(
() => ({
theme,
setTheme: (newTheme: Theme) => {
localStorage.setItem(storageKey, newTheme)
setTheme(newTheme)
},
}),
[theme, storageKey]
)
return (
<ThemeProviderContext.Provider {...props} value={value}>
{children}
</ThemeProviderContext.Provider>
)
}
// Hook to use the theme context
export const useTheme = () => {
const context = React.useContext(ThemeProviderContext)
if (context === undefined)
throw new Error("useTheme must be used within a ThemeProvider")
return context
}

View File

@ -0,0 +1,25 @@
"use client"
import * as React from "react"
import { Moon, Sun } from "lucide-react"
import { useTheme } from "@/components/ui/theme-provider"
import { Button } from "@/components/ui/button"
export function ThemeToggle() {
const { theme, setTheme } = useTheme()
return (
<Button
variant="ghost"
size="icon"
onClick={() => setTheme(theme === "dark" ? "light" : "dark")}
className="rounded-full"
aria-label="Toggle theme"
>
<Sun className="h-5 w-5 rotate-0 scale-100 transition-transform dark:-rotate-90 dark:scale-0" />
<Moon className="absolute h-5 w-5 rotate-90 scale-0 transition-transform dark:rotate-0 dark:scale-100" />
<span className="sr-only">Toggle theme</span>
</Button>
)
}

View File

@ -3,4 +3,15 @@ import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
}
// Check if we're running in the browser
export const isBrowser = typeof window !== 'undefined'
// Theme utilities
export type Theme = 'dark' | 'light' | 'system'
export function getSystemTheme(): Theme {
if (!isBrowser) return 'light'
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
}

View File

@ -1,5 +1,6 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
darkMode: ["class"],
content: [
'./src/pages/**/*.{js,ts,jsx,tsx,mdx}',
'./src/components/**/*.{js,ts,jsx,tsx,mdx}',