dark/light toggle
This commit is contained in:
parent
472396055f
commit
33d0923fc7
|
|
@ -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
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import type { Metadata } from 'next'
|
import type { Metadata } from 'next'
|
||||||
import { Inter } from 'next/font/google'
|
import { Inter } from 'next/font/google'
|
||||||
import './globals.css'
|
import './globals.css'
|
||||||
|
import { ThemeProvider } from '@/components/ui/theme-provider'
|
||||||
|
|
||||||
const inter = Inter({ subsets: ['latin'] })
|
const inter = Inter({ subsets: ['latin'] })
|
||||||
|
|
||||||
|
|
@ -15,9 +16,16 @@ export default function RootLayout({
|
||||||
children: React.ReactNode
|
children: React.ReactNode
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<html lang="en">
|
<html lang="en" suppressHydrationWarning>
|
||||||
<body className={inter.className}>
|
<body className={inter.className}>
|
||||||
{children}
|
<ThemeProvider
|
||||||
|
attribute="class"
|
||||||
|
defaultTheme="system"
|
||||||
|
enableSystem
|
||||||
|
disableTransitionOnChange
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</ThemeProvider>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
import Link from "next/link"
|
import Link from "next/link"
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
|
import { ThemeToggle } from "@/components/ui/theme-toggle"
|
||||||
import { motion } from "framer-motion"
|
import { motion } from "framer-motion"
|
||||||
|
|
||||||
function Navbar() {
|
function Navbar() {
|
||||||
|
|
@ -33,6 +34,7 @@ function Navbar() {
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
|
<ThemeToggle />
|
||||||
<Button variant="outline" className="hidden md:flex">
|
<Button variant="outline" className="hidden md:flex">
|
||||||
Connect Wallet
|
Connect Wallet
|
||||||
</Button>
|
</Button>
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -3,4 +3,15 @@ import { twMerge } from "tailwind-merge"
|
||||||
|
|
||||||
export function cn(...inputs: ClassValue[]) {
|
export function cn(...inputs: ClassValue[]) {
|
||||||
return twMerge(clsx(inputs))
|
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'
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
/** @type {import('tailwindcss').Config} */
|
/** @type {import('tailwindcss').Config} */
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
darkMode: ["class"],
|
||||||
content: [
|
content: [
|
||||||
'./src/pages/**/*.{js,ts,jsx,tsx,mdx}',
|
'./src/pages/**/*.{js,ts,jsx,tsx,mdx}',
|
||||||
'./src/components/**/*.{js,ts,jsx,tsx,mdx}',
|
'./src/components/**/*.{js,ts,jsx,tsx,mdx}',
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue