From 777693c7dde151d3ba18953088d5ea7a1224e4e3 Mon Sep 17 00:00:00 2001 From: rckrdmrd Date: Tue, 20 Jan 2026 02:27:08 -0600 Subject: [PATCH] [MCH-FE] feat: Add dark mode theme support - Add ThemeContext for system/light/dark theme management - Update Layout with theme toggle button - Add dark mode styles to components - Update Tailwind config for dark mode support - Add CSS transitions for smooth theme switching Co-Authored-By: Claude Opus 4.5 --- src/App.tsx | 65 +++++++++++---------- src/components/Layout.tsx | 78 ++++++++++++++++--------- src/contexts/ThemeContext.tsx | 103 ++++++++++++++++++++++++++++++++++ src/index.css | 10 +++- tailwind.config.js | 1 + 5 files changed, 200 insertions(+), 57 deletions(-) create mode 100644 src/contexts/ThemeContext.tsx diff --git a/src/App.tsx b/src/App.tsx index 94e72c0..4f92cae 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,6 +1,7 @@ import { BrowserRouter, Routes, Route, Navigate, Outlet } from 'react-router-dom'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { AuthProvider, useAuth } from './contexts/AuthContext'; +import { ThemeProvider } from './contexts/ThemeContext'; import { Layout } from './components/Layout'; import { Dashboard } from './pages/Dashboard'; import { Products } from './pages/Products'; @@ -59,39 +60,41 @@ function PublicRoute() { function App() { return ( - - - - {/* Public routes */} - }> - } /> - } /> - - - {/* Protected routes */} - }> - }> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> + + + + + {/* Public routes */} + }> + } /> + } /> - - {/* Catch all */} - } /> - - - + {/* Protected routes */} + }> + }> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + + + + {/* Catch all */} + } /> + + + + ); } diff --git a/src/components/Layout.tsx b/src/components/Layout.tsx index 0ef8465..d982806 100644 --- a/src/components/Layout.tsx +++ b/src/components/Layout.tsx @@ -16,10 +16,13 @@ import { Truck, Coins, QrCode, + Sun, + Moon, } from 'lucide-react'; import { useState } from 'react'; import clsx from 'clsx'; import { useAuth } from '../contexts/AuthContext'; +import { useTheme } from '../contexts/ThemeContext'; const navigation = [ { name: 'Dashboard', href: '/dashboard', icon: LayoutDashboard }, @@ -39,6 +42,7 @@ const navigation = [ export function Layout() { const [sidebarOpen, setSidebarOpen] = useState(false); const { user, tenant, logout } = useAuth(); + const { resolvedTheme, toggleTheme } = useTheme(); const navigate = useNavigate(); const handleLogout = () => { @@ -56,7 +60,7 @@ export function Layout() { }; return ( -
+
{/* Mobile sidebar */}
setSidebarOpen(false)} /> -
-
+
+
- MiChangarrito + MiChangarrito
-
@@ -88,8 +92,8 @@ export function Layout() { clsx( 'flex items-center gap-3 px-3 py-2 rounded-lg transition-colors', isActive - ? 'bg-primary-50 text-primary-600' - : 'text-gray-700 hover:bg-gray-100' + ? 'bg-primary-50 text-primary-600 dark:bg-primary-900/30 dark:text-primary-400' + : 'text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-700' ) } > @@ -103,12 +107,23 @@ export function Layout() { {/* Desktop sidebar */}
-
-
+
+
- MiChangarrito + MiChangarrito
+
-
+
-
- +
+ {tenant ? getInitials(tenant.name) : 'MC'}
-

{tenant?.name || 'Mi Negocio'}

-

{user?.name}

+

{tenant?.name || 'Mi Negocio'}

+

{user?.name}

-
- - MiChangarrito +
+
+ +
+ + MiChangarrito +
+
{/* Page content */} diff --git a/src/contexts/ThemeContext.tsx b/src/contexts/ThemeContext.tsx new file mode 100644 index 0000000..a6b0815 --- /dev/null +++ b/src/contexts/ThemeContext.tsx @@ -0,0 +1,103 @@ +import { createContext, useContext, useState, useEffect } from 'react'; +import type { ReactNode } from 'react'; + +type Theme = 'light' | 'dark' | 'system'; + +interface ThemeContextType { + theme: Theme; + resolvedTheme: 'light' | 'dark'; + setTheme: (theme: Theme) => void; + toggleTheme: () => void; +} + +const ThemeContext = createContext(undefined); + +const STORAGE_KEY = 'michangarrito-theme'; + +function getSystemTheme(): 'light' | 'dark' { + if (typeof window !== 'undefined' && window.matchMedia) { + return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; + } + return 'light'; +} + +function getStoredTheme(): Theme { + if (typeof window !== 'undefined') { + const stored = localStorage.getItem(STORAGE_KEY); + if (stored === 'light' || stored === 'dark' || stored === 'system') { + return stored; + } + } + return 'system'; +} + +export function ThemeProvider({ children }: { children: ReactNode }) { + const [theme, setThemeState] = useState(getStoredTheme); + const [resolvedTheme, setResolvedTheme] = useState<'light' | 'dark'>('light'); + + // Resolve the actual theme (handles 'system' preference) + useEffect(() => { + const resolved = theme === 'system' ? getSystemTheme() : theme; + setResolvedTheme(resolved); + + // Apply class to document + const root = document.documentElement; + if (resolved === 'dark') { + root.classList.add('dark'); + } else { + root.classList.remove('dark'); + } + }, [theme]); + + // Listen for system theme changes + useEffect(() => { + if (theme !== 'system') return; + + const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); + const handleChange = (e: MediaQueryListEvent) => { + const newTheme = e.matches ? 'dark' : 'light'; + setResolvedTheme(newTheme); + + const root = document.documentElement; + if (newTheme === 'dark') { + root.classList.add('dark'); + } else { + root.classList.remove('dark'); + } + }; + + mediaQuery.addEventListener('change', handleChange); + return () => mediaQuery.removeEventListener('change', handleChange); + }, [theme]); + + const setTheme = (newTheme: Theme) => { + setThemeState(newTheme); + localStorage.setItem(STORAGE_KEY, newTheme); + }; + + const toggleTheme = () => { + const newTheme = resolvedTheme === 'light' ? 'dark' : 'light'; + setTheme(newTheme); + }; + + return ( + + {children} + + ); +} + +export function useTheme() { + const context = useContext(ThemeContext); + if (context === undefined) { + throw new Error('useTheme must be used within a ThemeProvider'); + } + return context; +} diff --git a/src/index.css b/src/index.css index c48d4d6..107701b 100644 --- a/src/index.css +++ b/src/index.css @@ -26,7 +26,12 @@ @layer base { body { - @apply bg-gray-50 text-gray-900; + @apply bg-gray-50 text-gray-900 transition-colors duration-200; + } + + .dark body, + html.dark body { + @apply bg-gray-900 text-gray-100; } } @@ -41,13 +46,16 @@ .btn-outline { @apply border border-gray-300 hover:border-gray-400 text-gray-700 font-medium py-2 px-4 rounded-lg transition-colors; + @apply dark:border-gray-600 dark:hover:border-gray-500 dark:text-gray-300; } .card { @apply bg-white rounded-xl shadow-sm border border-gray-200 p-4; + @apply dark:bg-gray-800 dark:border-gray-700; } .input { @apply w-full rounded-lg border-gray-300 shadow-sm focus:border-primary-500 focus:ring-primary-500; + @apply dark:bg-gray-800 dark:border-gray-600 dark:text-gray-100 dark:placeholder-gray-400; } } diff --git a/tailwind.config.js b/tailwind.config.js index cc11449..af0fc77 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -1,5 +1,6 @@ /** @type {import('tailwindcss').Config} */ export default { + darkMode: 'class', content: [ "./index.html", "./src/**/*.{js,ts,jsx,tsx}",