From 12b8e2b4b56338512d4f25bb0e3f4f29b4970955 Mon Sep 17 00:00:00 2001 From: Jez Date: Sat, 8 Nov 2025 18:41:37 +1100 Subject: [PATCH] feat: implement dark mode with complete semantic color system MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Install next-themes package - Create ThemeProvider component with localStorage persistence - Create ModeToggle cycling button (Light → Dark → System) - Update layout.tsx: - Wrap app with ThemeProvider - Add suppressHydrationWarning to html tag - Replace bg-gray-50 with bg-background - Update navigation.tsx: - Add ModeToggle button next to LogoutButton - Replace bg-white with bg-card - Remove hard-coded text colors **Complete semantic color audit (54 violations fixed):** - dashboard.page.tsx: Replace all gray/blue/green/purple with semantic - todo-card.tsx: text-muted-foreground, text-primary, text-destructive - todo-list.page.tsx: text-muted-foreground variants - delete-todo.tsx: text/bg-destructive for delete actions - todo-form.tsx: border-border, text-primary for upload UI - edit-todo.page.tsx: text-muted-foreground - new-todo.page.tsx: text-muted-foreground Priority/status badge colors kept as-is (semantic data visualization). Dark mode now fully functional with proper theme-aware colors. --- package.json | 1 + pnpm-lock.yaml | 14 +++++++ src/app/layout.tsx | 16 +++++-- src/components/mode-toggle.tsx | 44 ++++++++++++++++++++ src/components/navigation.tsx | 10 +++-- src/components/theme-provider.tsx | 8 ++++ src/modules/dashboard/dashboard.page.tsx | 24 +++++------ src/modules/todos/components/delete-todo.tsx | 4 +- src/modules/todos/components/todo-card.tsx | 10 ++--- src/modules/todos/components/todo-form.tsx | 8 ++-- src/modules/todos/edit-todo.page.tsx | 2 +- src/modules/todos/new-todo.page.tsx | 2 +- src/modules/todos/todo-list.page.tsx | 8 ++-- 13 files changed, 115 insertions(+), 36 deletions(-) create mode 100644 src/components/mode-toggle.tsx create mode 100644 src/components/theme-provider.tsx diff --git a/package.json b/package.json index 7c5f3e8..5f76957 100644 --- a/package.json +++ b/package.json @@ -48,6 +48,7 @@ "drizzle-zod": "^0.8.3", "lucide-react": "^0.544.0", "next": "15.4.6", + "next-themes": "^0.4.6", "react": "19.1.0", "react-dom": "19.1.0", "react-hook-form": "^7.62.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 11a3696..b14f415 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -62,6 +62,9 @@ importers: next: specifier: 15.4.6 version: 15.4.6(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + next-themes: + specifier: ^0.4.6 + version: 0.4.6(react-dom@19.1.0(react@19.1.0))(react@19.1.0) react: specifier: 19.1.0 version: 19.1.0 @@ -3220,6 +3223,12 @@ packages: resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} engines: {node: '>= 0.6'} + next-themes@0.4.6: + resolution: {integrity: sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==} + peerDependencies: + react: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc + react-dom: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc + next@15.4.6: resolution: {integrity: sha512-us++E/Q80/8+UekzB3SAGs71AlLDsadpFMXVNM/uQ0BMwsh9m3mr0UNQIfjKed8vpWXsASe+Qifrnu1oLIcKEQ==} engines: {node: ^18.18.0 || ^19.8.0 || >= 20.0.0} @@ -7423,6 +7432,11 @@ snapshots: negotiator@1.0.0: {} + next-themes@0.4.6(react-dom@19.1.0(react@19.1.0))(react@19.1.0): + dependencies: + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + next@15.4.6(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0): dependencies: '@next/env': 15.4.6 diff --git a/src/app/layout.tsx b/src/app/layout.tsx index c4406ed..6ae61e4 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -2,6 +2,7 @@ import type { Metadata } from "next"; import { Geist, Geist_Mono } from "next/font/google"; import "./globals.css"; import { Toaster } from "react-hot-toast"; +import { ThemeProvider } from "@/components/theme-provider"; const geistSans = Geist({ variable: "--font-geist-sans", @@ -27,12 +28,19 @@ export default function RootLayout({ children: React.ReactNode; }>) { return ( - + -
{children}
- + +
{children}
+ +
); diff --git a/src/components/mode-toggle.tsx b/src/components/mode-toggle.tsx new file mode 100644 index 0000000..eb4d819 --- /dev/null +++ b/src/components/mode-toggle.tsx @@ -0,0 +1,44 @@ +"use client"; + +import { Moon, Sun, Monitor } from "lucide-react"; +import { useTheme } from "next-themes"; +import { useEffect, useState } from "react"; +import { Button } from "@/components/ui/button"; + +export function ModeToggle() { + const { theme, setTheme } = useTheme(); + const [mounted, setMounted] = useState(false); + + // Avoid hydration mismatch by only rendering after mount + useEffect(() => { + setMounted(true); + }, []); + + if (!mounted) { + return ( + + ); + } + + const cycleTheme = () => { + if (theme === "light") { + setTheme("dark"); + } else if (theme === "dark") { + setTheme("system"); + } else { + setTheme("light"); + } + }; + + return ( + + ); +} diff --git a/src/components/navigation.tsx b/src/components/navigation.tsx index a6a3b0f..04535c3 100644 --- a/src/components/navigation.tsx +++ b/src/components/navigation.tsx @@ -2,16 +2,17 @@ import { CheckSquare, Home } from "lucide-react"; import Link from "next/link"; import { Button } from "@/components/ui/button"; import LogoutButton from "../modules/auth/components/logout-button"; +import { ModeToggle } from "@/components/mode-toggle"; export function Navigation() { return ( -