feat: add dark mode toggle with theme persistence
All checks were successful
Basil CI/CD Pipeline / Code Linting (push) Successful in 58s
Basil CI/CD Pipeline / Web Tests (push) Successful in 1m5s
Basil CI/CD Pipeline / Shared Package Tests (push) Successful in 1m0s
Basil CI/CD Pipeline / Security Scanning (push) Successful in 1m6s
Basil CI/CD Pipeline / API Tests (push) Successful in 2m44s
Basil CI/CD Pipeline / Build All Packages (push) Successful in 3m9s
Basil CI/CD Pipeline / E2E Tests (push) Has been skipped
Basil CI/CD Pipeline / Build & Push Docker Images (push) Successful in 5m5s
Basil CI/CD Pipeline / Trigger Deployment (push) Successful in 11s
All checks were successful
Basil CI/CD Pipeline / Code Linting (push) Successful in 58s
Basil CI/CD Pipeline / Web Tests (push) Successful in 1m5s
Basil CI/CD Pipeline / Shared Package Tests (push) Successful in 1m0s
Basil CI/CD Pipeline / Security Scanning (push) Successful in 1m6s
Basil CI/CD Pipeline / API Tests (push) Successful in 2m44s
Basil CI/CD Pipeline / Build All Packages (push) Successful in 3m9s
Basil CI/CD Pipeline / E2E Tests (push) Has been skipped
Basil CI/CD Pipeline / Build & Push Docker Images (push) Successful in 5m5s
Basil CI/CD Pipeline / Trigger Deployment (push) Successful in 11s
Implemented a simple dark mode toggle that persists user preference across sessions. Changes: - Add CSS custom properties for light and dark themes in App.css - Create ThemeContext for global theme state management - Create ThemeToggle component with moon/sun icons - Update all color references to use CSS variables for theme support - Add localStorage persistence for theme preference - Include smooth transitions between themes The toggle appears in the header next to the user menu and allows instant switching between light and dark modes. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,3 +1,37 @@
|
|||||||
|
:root {
|
||||||
|
/* Light mode colors */
|
||||||
|
--bg-primary: #f5f5f5;
|
||||||
|
--bg-secondary: #ffffff;
|
||||||
|
--bg-tertiary: #f9f9f9;
|
||||||
|
--text-primary: #333333;
|
||||||
|
--text-secondary: #666666;
|
||||||
|
--text-tertiary: #999999;
|
||||||
|
--brand-primary: #2d5016;
|
||||||
|
--brand-secondary: #3d6821;
|
||||||
|
--brand-hover: #1f3710;
|
||||||
|
--border-color: #ddd;
|
||||||
|
--border-light: #eee;
|
||||||
|
--shadow: rgba(0,0,0,0.1);
|
||||||
|
--shadow-hover: rgba(0,0,0,0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] {
|
||||||
|
/* Dark mode colors */
|
||||||
|
--bg-primary: #1a1a1a;
|
||||||
|
--bg-secondary: #2d2d2d;
|
||||||
|
--bg-tertiary: #242424;
|
||||||
|
--text-primary: #e0e0e0;
|
||||||
|
--text-secondary: #b0b0b0;
|
||||||
|
--text-tertiary: #808080;
|
||||||
|
--brand-primary: #4a7c2d;
|
||||||
|
--brand-secondary: #5a8c3d;
|
||||||
|
--brand-hover: #3d6821;
|
||||||
|
--border-color: #404040;
|
||||||
|
--border-light: #333333;
|
||||||
|
--shadow: rgba(0,0,0,0.3);
|
||||||
|
--shadow-hover: rgba(0,0,0,0.4);
|
||||||
|
}
|
||||||
|
|
||||||
* {
|
* {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
@@ -8,7 +42,9 @@ body {
|
|||||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
|
||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
-moz-osx-font-smoothing: grayscale;
|
-moz-osx-font-smoothing: grayscale;
|
||||||
background-color: #f5f5f5;
|
background-color: var(--bg-primary);
|
||||||
|
color: var(--text-primary);
|
||||||
|
transition: background-color 0.3s ease, color 0.3s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.app {
|
.app {
|
||||||
@@ -25,10 +61,10 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.header {
|
.header {
|
||||||
background-color: #2d5016;
|
background-color: var(--brand-primary);
|
||||||
color: white;
|
color: white;
|
||||||
padding: 1rem 0;
|
padding: 1rem 0;
|
||||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
box-shadow: 0 2px 4px var(--shadow);
|
||||||
}
|
}
|
||||||
|
|
||||||
.header .container {
|
.header .container {
|
||||||
@@ -73,7 +109,7 @@ nav a:hover {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.footer {
|
.footer {
|
||||||
background-color: #2d5016;
|
background-color: var(--brand-primary);
|
||||||
color: white;
|
color: white;
|
||||||
padding: 1rem 0;
|
padding: 1rem 0;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
@@ -88,17 +124,17 @@ nav a:hover {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.recipe-card {
|
.recipe-card {
|
||||||
background: white;
|
background: var(--bg-secondary);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
box-shadow: 0 2px 8px var(--shadow);
|
||||||
transition: transform 0.2s;
|
transition: transform 0.2s;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.recipe-card:hover {
|
.recipe-card:hover {
|
||||||
transform: translateY(-4px);
|
transform: translateY(-4px);
|
||||||
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
box-shadow: 0 4px 12px var(--shadow-hover);
|
||||||
}
|
}
|
||||||
|
|
||||||
.recipe-card img {
|
.recipe-card img {
|
||||||
@@ -113,19 +149,19 @@ nav a:hover {
|
|||||||
|
|
||||||
.recipe-card h3 {
|
.recipe-card h3 {
|
||||||
margin-bottom: 0.5rem;
|
margin-bottom: 0.5rem;
|
||||||
color: #2d5016;
|
color: var(--brand-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.recipe-card p {
|
.recipe-card p {
|
||||||
color: #666;
|
color: var(--text-secondary);
|
||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.recipe-detail {
|
.recipe-detail {
|
||||||
background: white;
|
background: var(--bg-secondary);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
padding: 2rem;
|
padding: 2rem;
|
||||||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
box-shadow: 0 2px 8px var(--shadow);
|
||||||
}
|
}
|
||||||
|
|
||||||
.recipe-actions {
|
.recipe-actions {
|
||||||
@@ -151,7 +187,7 @@ nav a:hover {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.recipe-detail h2 {
|
.recipe-detail h2 {
|
||||||
color: #2d5016;
|
color: var(--brand-primary);
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -159,7 +195,7 @@ nav a:hover {
|
|||||||
display: flex;
|
display: flex;
|
||||||
gap: 2rem;
|
gap: 2rem;
|
||||||
margin-bottom: 2rem;
|
margin-bottom: 2rem;
|
||||||
color: #666;
|
color: var(--text-secondary);
|
||||||
align-items: center;
|
align-items: center;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
@@ -186,7 +222,7 @@ nav a:hover {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
background-color: #2d5016;
|
background-color: var(--brand-primary);
|
||||||
color: white;
|
color: white;
|
||||||
border: none;
|
border: none;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
@@ -194,7 +230,7 @@ nav a:hover {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.servings-adjuster button:hover:not(:disabled) {
|
.servings-adjuster button:hover:not(:disabled) {
|
||||||
background-color: #3d6821;
|
background-color: var(--brand-secondary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.servings-adjuster button:disabled {
|
.servings-adjuster button:disabled {
|
||||||
@@ -259,9 +295,9 @@ nav a:hover {
|
|||||||
.recipe-section {
|
.recipe-section {
|
||||||
margin-bottom: 3rem;
|
margin-bottom: 3rem;
|
||||||
padding: 1.5rem;
|
padding: 1.5rem;
|
||||||
background-color: #f9f9f9;
|
background-color: var(--bg-tertiary);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
border-left: 4px solid #2d5016;
|
border-left: 4px solid var(--brand-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.section-header {
|
.section-header {
|
||||||
@@ -274,12 +310,12 @@ nav a:hover {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.section-header h3 {
|
.section-header h3 {
|
||||||
color: #2d5016;
|
color: var(--brand-primary);
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.section-timing {
|
.section-timing {
|
||||||
background-color: #2d5016;
|
background-color: var(--brand-primary);
|
||||||
color: white;
|
color: white;
|
||||||
padding: 0.4rem 0.8rem;
|
padding: 0.4rem 0.8rem;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
@@ -288,13 +324,13 @@ nav a:hover {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.instruction-timing {
|
.instruction-timing {
|
||||||
color: #2d5016;
|
color: var(--brand-primary);
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
font-size: 0.95rem;
|
font-size: 0.95rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.recipe-section h4 {
|
.recipe-section h4 {
|
||||||
color: #2d5016;
|
color: var(--brand-primary);
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
font-size: 1.1rem;
|
font-size: 1.1rem;
|
||||||
}
|
}
|
||||||
@@ -304,7 +340,7 @@ nav a:hover {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.ingredients h3, .instructions h3 {
|
.ingredients h3, .instructions h3 {
|
||||||
color: #2d5016;
|
color: var(--brand-primary);
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -315,7 +351,7 @@ nav a:hover {
|
|||||||
|
|
||||||
.ingredients li {
|
.ingredients li {
|
||||||
padding: 0.5rem 0;
|
padding: 0.5rem 0;
|
||||||
border-bottom: 1px solid #eee;
|
border-bottom: 1px solid var(--border-light);
|
||||||
}
|
}
|
||||||
|
|
||||||
.instructions ol {
|
.instructions ol {
|
||||||
@@ -335,16 +371,18 @@ nav a:hover {
|
|||||||
display: block;
|
display: block;
|
||||||
margin-bottom: 0.5rem;
|
margin-bottom: 0.5rem;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
color: #333;
|
color: var(--text-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-group input,
|
.form-group input,
|
||||||
.form-group textarea {
|
.form-group textarea {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 0.75rem;
|
padding: 0.75rem;
|
||||||
border: 1px solid #ddd;
|
border: 1px solid var(--border-color);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
|
background-color: var(--bg-secondary);
|
||||||
|
color: var(--text-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-group textarea {
|
.form-group textarea {
|
||||||
@@ -353,7 +391,7 @@ nav a:hover {
|
|||||||
}
|
}
|
||||||
|
|
||||||
button {
|
button {
|
||||||
background-color: #2d5016;
|
background-color: var(--brand-primary);
|
||||||
color: white;
|
color: white;
|
||||||
border: none;
|
border: none;
|
||||||
padding: 0.75rem 1.5rem;
|
padding: 0.75rem 1.5rem;
|
||||||
@@ -364,7 +402,7 @@ button {
|
|||||||
}
|
}
|
||||||
|
|
||||||
button:hover {
|
button:hover {
|
||||||
background-color: #1f3710;
|
background-color: var(--brand-hover);
|
||||||
}
|
}
|
||||||
|
|
||||||
button:disabled {
|
button:disabled {
|
||||||
@@ -383,15 +421,15 @@ button:disabled {
|
|||||||
.loading {
|
.loading {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 2rem;
|
padding: 2rem;
|
||||||
color: #666;
|
color: var(--text-secondary);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Recipe Form Styles */
|
/* Recipe Form Styles */
|
||||||
.recipe-form {
|
.recipe-form {
|
||||||
background: white;
|
background: var(--bg-secondary);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
padding: 2rem;
|
padding: 2rem;
|
||||||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
box-shadow: 0 2px 8px var(--shadow);
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-row {
|
.form-row {
|
||||||
@@ -417,9 +455,9 @@ button:disabled {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.section-form {
|
.section-form {
|
||||||
background-color: #f9f9f9;
|
background-color: var(--bg-tertiary);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
border-left: 4px solid #2d5016;
|
border-left: 4px solid var(--brand-primary);
|
||||||
padding: 1.5rem;
|
padding: 1.5rem;
|
||||||
margin-bottom: 2rem;
|
margin-bottom: 2rem;
|
||||||
}
|
}
|
||||||
@@ -433,17 +471,17 @@ button:disabled {
|
|||||||
|
|
||||||
.section-form-header h4 {
|
.section-form-header h4 {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
color: #2d5016;
|
color: var(--brand-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.subsection {
|
.subsection {
|
||||||
margin-top: 1.5rem;
|
margin-top: 1.5rem;
|
||||||
padding-top: 1rem;
|
padding-top: 1rem;
|
||||||
border-top: 1px solid #ddd;
|
border-top: 1px solid var(--border-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.subsection h5 {
|
.subsection h5 {
|
||||||
color: #2d5016;
|
color: var(--brand-primary);
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -467,7 +505,7 @@ button:disabled {
|
|||||||
gap: 0.75rem;
|
gap: 0.75rem;
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
background-color: white;
|
background-color: var(--bg-secondary);
|
||||||
padding: 0.75rem;
|
padding: 0.75rem;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
border: 2px solid transparent;
|
border: 2px solid transparent;
|
||||||
@@ -475,20 +513,20 @@ button:disabled {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.instruction-row:hover {
|
.instruction-row:hover {
|
||||||
border-color: #e0e0e0;
|
border-color: var(--border-color);
|
||||||
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
|
box-shadow: 0 2px 4px var(--shadow);
|
||||||
}
|
}
|
||||||
|
|
||||||
.instruction-row.dragging {
|
.instruction-row.dragging {
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
background-color: #f5f5f5;
|
background-color: var(--bg-primary);
|
||||||
box-shadow: 0 4px 8px rgba(0,0,0,0.15);
|
box-shadow: 0 4px 8px var(--shadow-hover);
|
||||||
border-color: #2d5016;
|
border-color: var(--brand-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.instruction-drag-handle {
|
.instruction-drag-handle {
|
||||||
cursor: grab;
|
cursor: grab;
|
||||||
color: #999;
|
color: var(--text-tertiary);
|
||||||
font-size: 1.2rem;
|
font-size: 1.2rem;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -498,7 +536,7 @@ button:disabled {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.instruction-drag-handle:hover {
|
.instruction-drag-handle:hover {
|
||||||
color: #2d5016;
|
color: var(--brand-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.instruction-drag-handle:active {
|
.instruction-drag-handle:active {
|
||||||
@@ -508,7 +546,7 @@ button:disabled {
|
|||||||
.instruction-number {
|
.instruction-number {
|
||||||
min-width: 30px;
|
min-width: 30px;
|
||||||
height: 30px;
|
height: 30px;
|
||||||
background-color: #2d5016;
|
background-color: var(--brand-primary);
|
||||||
color: white;
|
color: white;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -527,9 +565,11 @@ button:disabled {
|
|||||||
|
|
||||||
.instruction-timing-input {
|
.instruction-timing-input {
|
||||||
padding: 0.5rem;
|
padding: 0.5rem;
|
||||||
border: 1px solid #ddd;
|
border: 1px solid var(--border-color);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
|
background-color: var(--bg-secondary);
|
||||||
|
color: var(--text-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-secondary {
|
.btn-secondary {
|
||||||
@@ -586,7 +626,7 @@ button:disabled {
|
|||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
margin-top: 2rem;
|
margin-top: 2rem;
|
||||||
padding-top: 2rem;
|
padding-top: 2rem;
|
||||||
border-top: 2px solid #eee;
|
border-top: 2px solid var(--border-light);
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-section {
|
.form-section {
|
||||||
@@ -596,9 +636,9 @@ button:disabled {
|
|||||||
/* Image Upload Styles */
|
/* Image Upload Styles */
|
||||||
.image-upload-section {
|
.image-upload-section {
|
||||||
padding: 1.5rem;
|
padding: 1.5rem;
|
||||||
background-color: #f9f9f9;
|
background-color: var(--bg-tertiary);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
border: 1px solid #ddd;
|
border: 1px solid var(--border-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.current-image {
|
.current-image {
|
||||||
@@ -705,7 +745,7 @@ button:disabled {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
margin-bottom: 0.75rem;
|
margin-bottom: 0.75rem;
|
||||||
background-color: #f5f5f5;
|
background-color: var(--bg-tertiary);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.2s;
|
transition: all 0.2s;
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import { BrowserRouter as Router, Routes, Route, Link } from 'react-router-dom';
|
import { BrowserRouter as Router, Routes, Route, Link } from 'react-router-dom';
|
||||||
import { AuthProvider } from './contexts/AuthContext';
|
import { AuthProvider } from './contexts/AuthContext';
|
||||||
|
import { ThemeProvider } from './contexts/ThemeContext';
|
||||||
import ProtectedRoute from './components/ProtectedRoute';
|
import ProtectedRoute from './components/ProtectedRoute';
|
||||||
import UserMenu from './components/UserMenu';
|
import UserMenu from './components/UserMenu';
|
||||||
|
import ThemeToggle from './components/ThemeToggle';
|
||||||
import Login from './pages/Login';
|
import Login from './pages/Login';
|
||||||
import Register from './pages/Register';
|
import Register from './pages/Register';
|
||||||
import AuthCallback from './pages/AuthCallback';
|
import AuthCallback from './pages/AuthCallback';
|
||||||
@@ -19,20 +21,24 @@ import './App.css';
|
|||||||
function App() {
|
function App() {
|
||||||
return (
|
return (
|
||||||
<Router>
|
<Router>
|
||||||
<AuthProvider>
|
<ThemeProvider>
|
||||||
<div className="app">
|
<AuthProvider>
|
||||||
<header className="header">
|
<div className="app">
|
||||||
<div className="container">
|
<header className="header">
|
||||||
<h1 className="logo"><Link to="/">🌿 Basil</Link></h1>
|
<div className="container">
|
||||||
<nav>
|
<h1 className="logo"><Link to="/">🌿 Basil</Link></h1>
|
||||||
<Link to="/">Cookbooks</Link>
|
<nav>
|
||||||
<Link to="/recipes">All Recipes</Link>
|
<Link to="/">Cookbooks</Link>
|
||||||
<Link to="/recipes/new">New Recipe</Link>
|
<Link to="/recipes">All Recipes</Link>
|
||||||
<Link to="/recipes/import">Import Recipe</Link>
|
<Link to="/recipes/new">New Recipe</Link>
|
||||||
</nav>
|
<Link to="/recipes/import">Import Recipe</Link>
|
||||||
<UserMenu />
|
</nav>
|
||||||
</div>
|
<div style={{ display: 'flex', alignItems: 'center', gap: '1rem' }}>
|
||||||
</header>
|
<ThemeToggle />
|
||||||
|
<UserMenu />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
<main className="main">
|
<main className="main">
|
||||||
<div className="container">
|
<div className="container">
|
||||||
@@ -62,7 +68,8 @@ function App() {
|
|||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
</AuthProvider>
|
</AuthProvider>
|
||||||
|
</ThemeProvider>
|
||||||
</Router>
|
</Router>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
24
packages/web/src/components/ThemeToggle.tsx
Normal file
24
packages/web/src/components/ThemeToggle.tsx
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import { useTheme } from '../contexts/ThemeContext';
|
||||||
|
import '../styles/ThemeToggle.css';
|
||||||
|
|
||||||
|
function ThemeToggle() {
|
||||||
|
try {
|
||||||
|
const { theme, toggleTheme } = useTheme();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
onClick={toggleTheme}
|
||||||
|
className="theme-toggle"
|
||||||
|
aria-label={`Switch to ${theme === 'light' ? 'dark' : 'light'} mode`}
|
||||||
|
title={`Switch to ${theme === 'light' ? 'dark' : 'light'} mode`}
|
||||||
|
>
|
||||||
|
{theme === 'light' ? '🌙' : '☀️'}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('ThemeToggle error:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ThemeToggle;
|
||||||
53
packages/web/src/contexts/ThemeContext.tsx
Normal file
53
packages/web/src/contexts/ThemeContext.tsx
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import { createContext, useContext, useState, useEffect, ReactNode } from 'react';
|
||||||
|
|
||||||
|
type Theme = 'light' | 'dark';
|
||||||
|
|
||||||
|
interface ThemeContextType {
|
||||||
|
theme: Theme;
|
||||||
|
toggleTheme: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
|
||||||
|
|
||||||
|
const THEME_STORAGE_KEY = 'basil_theme';
|
||||||
|
|
||||||
|
export function ThemeProvider({ children }: { children: ReactNode }) {
|
||||||
|
const [theme, setTheme] = useState<Theme>(() => {
|
||||||
|
// Load theme from localStorage safely
|
||||||
|
try {
|
||||||
|
const saved = localStorage.getItem(THEME_STORAGE_KEY);
|
||||||
|
return (saved === 'dark' ? 'dark' : 'light') as Theme;
|
||||||
|
} catch (e) {
|
||||||
|
return 'light';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Apply theme to document
|
||||||
|
document.documentElement.setAttribute('data-theme', theme);
|
||||||
|
// Save to localStorage safely
|
||||||
|
try {
|
||||||
|
localStorage.setItem(THEME_STORAGE_KEY, theme);
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Failed to save theme preference:', e);
|
||||||
|
}
|
||||||
|
}, [theme]);
|
||||||
|
|
||||||
|
const toggleTheme = () => {
|
||||||
|
setTheme(prev => prev === 'light' ? 'dark' : 'light');
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ThemeContext.Provider value={{ theme, toggleTheme }}>
|
||||||
|
{children}
|
||||||
|
</ThemeContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useTheme() {
|
||||||
|
const context = useContext(ThemeContext);
|
||||||
|
if (context === undefined) {
|
||||||
|
throw new Error('useTheme must be used within a ThemeProvider');
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
}
|
||||||
23
packages/web/src/styles/ThemeToggle.css
Normal file
23
packages/web/src/styles/ThemeToggle.css
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
.theme-toggle {
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border-radius: 8px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
padding: 0;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-toggle:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.3);
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-toggle:active {
|
||||||
|
transform: scale(0.95);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user