From 33d0923fc7238199b2326267abaf0c5d27782839 Mon Sep 17 00:00:00 2001 From: boilerrat <128boilerrat@gmail.com> Date: Sun, 9 Mar 2025 05:55:32 -0400 Subject: [PATCH] dark/light toggle --- CLAUDE.md | 22 +++++++ src/app/layout.tsx | 12 +++- src/components/navbar.tsx | 2 + src/components/ui/theme-provider.tsx | 97 ++++++++++++++++++++++++++++ src/components/ui/theme-toggle.tsx | 25 +++++++ src/lib/utils.ts | 13 +++- tailwind.config.js | 1 + 7 files changed, 169 insertions(+), 3 deletions(-) create mode 100644 CLAUDE.md create mode 100644 src/components/ui/theme-provider.tsx create mode 100644 src/components/ui/theme-toggle.tsx diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..b088c15 --- /dev/null +++ b/CLAUDE.md @@ -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 \ No newline at end of file diff --git a/src/app/layout.tsx b/src/app/layout.tsx index d55a7cd..cc3d35b 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -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 ( - + - {children} + + {children} + ) diff --git a/src/components/navbar.tsx b/src/components/navbar.tsx index e92c107..f7c929c 100644 --- a/src/components/navbar.tsx +++ b/src/components/navbar.tsx @@ -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() {
+ diff --git a/src/components/ui/theme-provider.tsx b/src/components/ui/theme-provider.tsx new file mode 100644 index 0000000..e740035 --- /dev/null +++ b/src/components/ui/theme-provider.tsx @@ -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(initialState) + +export function ThemeProvider({ + children, + defaultTheme = "system", + storageKey = "theme", + ...props +}: ThemeProviderProps) { + const [theme, setTheme] = React.useState( + () => { + 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 ( + + {children} + + ) +} + +// 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 +} \ No newline at end of file diff --git a/src/components/ui/theme-toggle.tsx b/src/components/ui/theme-toggle.tsx new file mode 100644 index 0000000..9ee7dfe --- /dev/null +++ b/src/components/ui/theme-toggle.tsx @@ -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 ( + + ) +} \ No newline at end of file diff --git a/src/lib/utils.ts b/src/lib/utils.ts index f7654e8..c340e04 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -3,4 +3,15 @@ import { twMerge } from "tailwind-merge" export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)) -} \ No newline at end of file +} + +// 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' +} \ No newline at end of file diff --git a/tailwind.config.js b/tailwind.config.js index 626f9b8..682d40c 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -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}',