feat(theme): implement full dark and light mode switching with ThemeContext and persistent storage
This commit is contained in:
parent
d1b2ac4eef
commit
eeccd176a1
6 changed files with 231 additions and 40 deletions
|
|
@ -1,4 +1,5 @@
|
|||
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'
|
||||
import { ThemeProvider } from './contexts/ThemeContext'
|
||||
import { AuthProvider } from './contexts/AuthContext'
|
||||
import { TimerProvider } from './contexts/TimerContext'
|
||||
import { LandingPage } from './pages/LandingPage'
|
||||
|
|
@ -18,6 +19,7 @@ import { ProfilePage } from './pages/ProfilePage'
|
|||
export function App() {
|
||||
return (
|
||||
<BrowserRouter>
|
||||
<ThemeProvider>
|
||||
<AuthProvider>
|
||||
<TimerProvider>
|
||||
<Routes>
|
||||
|
|
@ -51,6 +53,7 @@ export function App() {
|
|||
</Routes>
|
||||
</TimerProvider>
|
||||
</AuthProvider>
|
||||
</ThemeProvider>
|
||||
</BrowserRouter>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
import { Link } from "react-router-dom"
|
||||
import { Button, Chip, ChipLabel } from "@heroui/react"
|
||||
import { ArrowRight } from "lucide-react"
|
||||
import { ArrowRight, Sun, Moon } from "lucide-react"
|
||||
import { useTheme } from "../../contexts/ThemeContext"
|
||||
|
||||
export const Navbar = () => {
|
||||
const { theme, toggleTheme } = useTheme()
|
||||
return (
|
||||
<header className="sticky top-0 z-50 backdrop-blur-md bg-surface/85 border-b border-border-soft transition-all duration-200">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 h-20 flex items-center justify-between">
|
||||
|
|
@ -62,6 +64,20 @@ export const Navbar = () => {
|
|||
|
||||
{/* Actions */}
|
||||
<div className="flex items-center gap-3">
|
||||
<button
|
||||
type="button"
|
||||
onClick={toggleTheme}
|
||||
title={theme === 'dark' ? 'Mudar para tema claro' : 'Mudar para tema escuro'}
|
||||
className="p-2 rounded-xl border border-border bg-surface text-muted hover:text-foreground hover:bg-surface-raised transition-colors shadow-xs"
|
||||
aria-label="Alternar tema"
|
||||
>
|
||||
{theme === 'dark' ? (
|
||||
<Sun className="w-4 h-4 text-amber-400" />
|
||||
) : (
|
||||
<Moon className="w-4 h-4 text-brand" />
|
||||
)}
|
||||
</button>
|
||||
|
||||
<Link
|
||||
to="/login"
|
||||
className="text-sm font-semibold text-text-main hover:text-brand px-3 py-2 transition-colors"
|
||||
|
|
|
|||
|
|
@ -17,11 +17,14 @@ import {
|
|||
PlusCircle,
|
||||
Menu as MenuIcon,
|
||||
Check,
|
||||
Square,
|
||||
Sun,
|
||||
Moon,
|
||||
} from 'lucide-react'
|
||||
import { useAuth } from '../../contexts/AuthContext'
|
||||
import { useTheme } from '../../contexts/ThemeContext'
|
||||
import { useTimer } from '../../contexts/TimerContext'
|
||||
import { formatSecondsToTimer } from '../../lib/timeUtils'
|
||||
import { Square } from 'lucide-react'
|
||||
|
||||
interface HeaderProps {
|
||||
onToggleMobileSidebar: () => void
|
||||
|
|
@ -29,6 +32,7 @@ interface HeaderProps {
|
|||
|
||||
export const Header = ({ onToggleMobileSidebar }: HeaderProps) => {
|
||||
const { user, activeOrg, setActiveOrg, logout } = useAuth()
|
||||
const { theme, toggleTheme } = useTheme()
|
||||
const { activeTask, activeSeconds, stopTimer } = useTimer()
|
||||
const [showOrgDropdown, setShowOrgDropdown] = useState(false)
|
||||
const [showUserDropdown, setShowUserDropdown] = useState(false)
|
||||
|
|
@ -206,6 +210,21 @@ export const Header = ({ onToggleMobileSidebar }: HeaderProps) => {
|
|||
<span className="hidden sm:inline">Tarefas</span>
|
||||
</Button>
|
||||
|
||||
{/* Theme Toggle Button */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={toggleTheme}
|
||||
title={theme === 'dark' ? 'Mudar para modo claro' : 'Mudar para modo escuro'}
|
||||
className="p-2 rounded-xl border border-border bg-surface text-muted hover:text-foreground hover:bg-surface-raised transition-colors shadow-xs flex items-center justify-center"
|
||||
aria-label="Alternar tema claro e escuro"
|
||||
>
|
||||
{theme === 'dark' ? (
|
||||
<Sun className="w-4 h-4 text-amber-400 hover:rotate-45 transition-transform" />
|
||||
) : (
|
||||
<Moon className="w-4 h-4 text-brand hover:-rotate-12 transition-transform" />
|
||||
)}
|
||||
</button>
|
||||
|
||||
{/* User Profile Menu Dropdown */}
|
||||
<div ref={userRef} className="relative">
|
||||
<button
|
||||
|
|
@ -270,6 +289,24 @@ export const Header = ({ onToggleMobileSidebar }: HeaderProps) => {
|
|||
<Settings className="w-4 h-4 text-brand" />
|
||||
Configurações do Workspace
|
||||
</Link>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={toggleTheme}
|
||||
className="w-full flex items-center justify-between p-2 rounded-xl text-xs font-semibold text-text-muted hover:text-text-main hover:bg-surface-muted transition-colors text-left"
|
||||
>
|
||||
<div className="flex items-center gap-2.5">
|
||||
{theme === 'dark' ? (
|
||||
<Sun className="w-4 h-4 text-amber-400" />
|
||||
) : (
|
||||
<Moon className="w-4 h-4 text-brand" />
|
||||
)}
|
||||
<span>Tema {theme === 'dark' ? 'Claro' : 'Escuro'}</span>
|
||||
</div>
|
||||
<span className="text-[10px] uppercase font-bold text-muted bg-surface-raised px-1.5 py-0.5 rounded-md border border-border">
|
||||
{theme}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="border-t border-border-soft pt-1 mt-1">
|
||||
|
|
|
|||
|
|
@ -9,9 +9,12 @@ import {
|
|||
Settings,
|
||||
X,
|
||||
Building2,
|
||||
Sun,
|
||||
Moon,
|
||||
} from 'lucide-react'
|
||||
import { Chip, ChipLabel } from '@heroui/react'
|
||||
import { useAuth } from '../../contexts/AuthContext'
|
||||
import { useTheme } from '../../contexts/ThemeContext'
|
||||
|
||||
interface SidebarProps {
|
||||
isMobileOpen: boolean
|
||||
|
|
@ -20,6 +23,7 @@ interface SidebarProps {
|
|||
|
||||
export const Sidebar = ({ isMobileOpen, onCloseMobile }: SidebarProps) => {
|
||||
const { activeOrg } = useAuth()
|
||||
const { theme, toggleTheme } = useTheme()
|
||||
|
||||
const navItems = [
|
||||
{
|
||||
|
|
@ -111,7 +115,8 @@ export const Sidebar = ({ isMobileOpen, onCloseMobile }: SidebarProps) => {
|
|||
|
||||
{/* Bottom Workspace Card Info */}
|
||||
<div className="p-4 border-t border-border-soft">
|
||||
<div className="p-3 rounded-2xl bg-surface-muted/70 border border-border-soft flex items-center gap-3">
|
||||
<div className="p-3 rounded-2xl bg-surface-muted/70 border border-border-soft flex items-center justify-between gap-2">
|
||||
<div className="flex items-center gap-2.5 truncate flex-1 min-w-0">
|
||||
<div className="w-8 h-8 rounded-xl bg-brand-soft text-brand flex items-center justify-center font-bold text-xs shrink-0">
|
||||
<Building2 className="w-4 h-4 text-brand" />
|
||||
</div>
|
||||
|
|
@ -124,6 +129,21 @@ export const Sidebar = ({ isMobileOpen, onCloseMobile }: SidebarProps) => {
|
|||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={toggleTheme}
|
||||
title={theme === 'dark' ? 'Mudar para tema claro' : 'Mudar para tema escuro'}
|
||||
className="p-1.5 rounded-lg border border-border-soft bg-surface text-muted hover:text-foreground hover:bg-surface-raised transition-colors shrink-0"
|
||||
aria-label="Alternar tema"
|
||||
>
|
||||
{theme === 'dark' ? (
|
||||
<Sun className="w-3.5 h-3.5 text-amber-400" />
|
||||
) : (
|
||||
<Moon className="w-3.5 h-3.5 text-brand" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
|
|
|||
56
new-frontend/src/contexts/ThemeContext.tsx
Normal file
56
new-frontend/src/contexts/ThemeContext.tsx
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
import React, { createContext, useContext, useState, useEffect } from 'react'
|
||||
|
||||
type Theme = 'light' | 'dark'
|
||||
|
||||
interface ThemeContextType {
|
||||
theme: Theme
|
||||
toggleTheme: () => void
|
||||
setTheme: (theme: Theme) => void
|
||||
}
|
||||
|
||||
const ThemeContext = createContext<ThemeContextType | undefined>(undefined)
|
||||
|
||||
export const ThemeProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||
const [theme, setThemeState] = useState<Theme>(() => {
|
||||
const saved = localStorage.getItem('bee_theme') as Theme | null
|
||||
if (saved === 'light' || saved === 'dark') {
|
||||
return saved
|
||||
}
|
||||
// Default to dark theme for modern developer dashboard experience
|
||||
return 'dark'
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
const root = document.documentElement
|
||||
if (theme === 'dark') {
|
||||
root.classList.add('dark')
|
||||
root.classList.remove('light')
|
||||
} else {
|
||||
root.classList.remove('dark')
|
||||
root.classList.add('light')
|
||||
}
|
||||
localStorage.setItem('bee_theme', theme)
|
||||
}, [theme])
|
||||
|
||||
const toggleTheme = () => {
|
||||
setThemeState((prev) => (prev === 'dark' ? 'light' : 'dark'))
|
||||
}
|
||||
|
||||
const setTheme = (newTheme: Theme) => {
|
||||
setThemeState(newTheme)
|
||||
}
|
||||
|
||||
return (
|
||||
<ThemeContext.Provider value={{ theme, toggleTheme, setTheme }}>
|
||||
{children}
|
||||
</ThemeContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export const useTheme = (): ThemeContextType => {
|
||||
const ctx = useContext(ThemeContext)
|
||||
if (!ctx) {
|
||||
throw new Error('useTheme must be used within a ThemeProvider')
|
||||
}
|
||||
return ctx
|
||||
}
|
||||
|
|
@ -1,37 +1,85 @@
|
|||
@import "tailwindcss";
|
||||
@config "../tailwind.config.js";
|
||||
|
||||
:root {
|
||||
--app-bg: #f5f1e8;
|
||||
--app-bg-strong: #ebe4d7;
|
||||
--surface: #ffffff;
|
||||
--surface-muted: #f6efe2;
|
||||
--surface-raised: #fbf8f1;
|
||||
--surface-sunken: #ece5d6;
|
||||
--border: #e2d7c3;
|
||||
--border-soft: #e7dcc9;
|
||||
--border-strong: #cdbba0;
|
||||
--text-main: #1c1917;
|
||||
--text-muted: #78716c;
|
||||
--foreground: #1c1917;
|
||||
--muted: #78716c;
|
||||
--brand-soft: #ccfbf1;
|
||||
--accent-soft: #ffedd5;
|
||||
--shadow-panel: 0 22px 60px rgba(79, 62, 35, 0.08), 0 6px 18px rgba(79, 62, 35, 0.06);
|
||||
color-scheme: light;
|
||||
}
|
||||
|
||||
.dark {
|
||||
--app-bg: #090d16;
|
||||
--app-bg-strong: #0f141f;
|
||||
--surface: #111726;
|
||||
--surface-muted: #171e30;
|
||||
--surface-raised: #182033;
|
||||
--surface-sunken: #0c111c;
|
||||
--border: #1e293b;
|
||||
--border-soft: #1e293b;
|
||||
--border-strong: #334155;
|
||||
--text-main: #f8fafc;
|
||||
--text-muted: #94a3b8;
|
||||
--foreground: #f8fafc;
|
||||
--muted: #94a3b8;
|
||||
--brand-soft: rgba(15, 118, 110, 0.2);
|
||||
--accent-soft: rgba(217, 119, 6, 0.2);
|
||||
--shadow-panel: 0 22px 60px rgba(0, 0, 0, 0.4), 0 6px 18px rgba(0, 0, 0, 0.25);
|
||||
color-scheme: dark;
|
||||
}
|
||||
|
||||
@theme {
|
||||
--font-sans: 'Plus Jakarta Sans', ui-sans-serif, system-ui, sans-serif;
|
||||
--font-mono: 'JetBrains Mono', ui-monospace, monospace;
|
||||
|
||||
--color-app-bg: #f5f1e8;
|
||||
--color-app-bg-strong: #ebe4d7;
|
||||
--color-surface: #fffdf8;
|
||||
--color-surface-muted: #f6efe2;
|
||||
--color-border-soft: #e7dcc9;
|
||||
--color-border-strong: #cdbba0;
|
||||
--color-text-main: #1f2937;
|
||||
--color-text-muted: #6b7280;
|
||||
--color-app-bg: var(--app-bg);
|
||||
--color-app-bg-strong: var(--app-bg-strong);
|
||||
--color-surface: var(--surface);
|
||||
--color-surface-muted: var(--surface-muted);
|
||||
--color-surface-raised: var(--surface-raised);
|
||||
--color-surface-sunken: var(--surface-sunken);
|
||||
--color-border: var(--border);
|
||||
--color-border-soft: var(--border-soft);
|
||||
--color-border-strong: var(--border-strong);
|
||||
--color-text-main: var(--text-main);
|
||||
--color-text-muted: var(--text-muted);
|
||||
--color-foreground: var(--foreground);
|
||||
--color-muted: var(--muted);
|
||||
|
||||
--color-brand: #0f766e;
|
||||
--color-brand-hover: #115e59;
|
||||
--color-brand-strong: #115e59;
|
||||
--color-brand-soft: #ccfbf1;
|
||||
--color-brand-soft: var(--brand-soft);
|
||||
|
||||
--color-accent: #d97706;
|
||||
--color-accent-soft: #ffedd5;
|
||||
--color-accent-hover: #b45309;
|
||||
--color-accent-soft: var(--accent-soft);
|
||||
|
||||
--color-success: #10b981;
|
||||
--color-success-soft: #ecfdf5;
|
||||
--color-success-soft: rgba(16, 185, 129, 0.15);
|
||||
--color-warning: #f59e0b;
|
||||
--color-warning-soft: #fffbeb;
|
||||
--color-warning-soft: rgba(245, 158, 11, 0.15);
|
||||
--color-danger: #ef4444;
|
||||
--color-danger-soft: #fef2f2;
|
||||
--color-danger-soft: rgba(239, 68, 68, 0.15);
|
||||
|
||||
--shadow-panel:
|
||||
0 22px 60px rgba(79, 62, 35, 0.08), 0 6px 18px rgba(79, 62, 35, 0.06);
|
||||
--shadow-panel: var(--shadow-panel);
|
||||
}
|
||||
|
||||
:root {
|
||||
color: var(--color-text-main);
|
||||
color: var(--color-foreground);
|
||||
background-color: var(--color-app-bg);
|
||||
font-family: var(--font-sans);
|
||||
font-synthesis: none;
|
||||
|
|
@ -52,8 +100,19 @@ body,
|
|||
|
||||
body {
|
||||
margin: 0;
|
||||
background:
|
||||
radial-gradient(circle at top left, rgba(217, 119, 6, 0.12), transparent 28%),
|
||||
radial-gradient(circle at top right, rgba(15, 118, 110, 0.14), transparent 24%),
|
||||
linear-gradient(180deg, var(--color-app-bg) 0%, #f8f5ef 100%);
|
||||
transition: background-color 0.2s ease, color 0.2s ease;
|
||||
}
|
||||
|
||||
html.dark body {
|
||||
background:
|
||||
radial-gradient(circle at top left, rgba(217, 119, 6, 0.08), transparent 28%),
|
||||
radial-gradient(circle at top right, rgba(15, 118, 110, 0.12), transparent 24%),
|
||||
linear-gradient(180deg, var(--app-bg) 0%, #060910 100%);
|
||||
}
|
||||
|
||||
html:not(.dark) body {
|
||||
background:
|
||||
radial-gradient(circle at top left, rgba(217, 119, 6, 0.08), transparent 28%),
|
||||
radial-gradient(circle at top right, rgba(15, 118, 110, 0.08), transparent 24%),
|
||||
linear-gradient(180deg, var(--app-bg) 0%, #faf7f0 100%);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue