feat: implement theme system and UI updates

- Added theme context and provider for light/dark mode support.
- Integrated theme toggle button in the Header component.
- Updated UI components to utilize CSS variables for consistent theming.
- Enhanced Kanban components and forms with new theme styles for better visual coherence.
- Adjusted global styles to define color variables for both themes, improving maintainability.
This commit is contained in:
Julien Froidefond
2025-09-15 11:49:54 +02:00
parent dce11e0569
commit 07cd3bde3b
23 changed files with 298 additions and 160 deletions

View File

@@ -1,8 +1,39 @@
@import "tailwindcss";
:root {
/* Dark theme (default) */
--background: #020617; /* slate-950 */
--foreground: #f1f5f9; /* slate-100 */
--card: #0f172a; /* slate-900 */
--card-hover: #1e293b; /* slate-800 */
--card-column: #0f172a; /* slate-900 - same as card in dark */
--border: #334155; /* slate-700 */
--input: #1e293b; /* slate-800 */
--primary: #06b6d4; /* cyan-500 */
--primary-foreground: #f1f5f9; /* slate-100 */
--muted: #475569; /* slate-600 */
--muted-foreground: #94a3b8; /* slate-400 */
--accent: #f59e0b; /* amber-500 */
--destructive: #ef4444; /* red-500 */
--success: #10b981; /* emerald-500 */
}
.light {
/* Light theme */
--background: #f1f5f9; /* slate-100 */
--foreground: #0f172a; /* slate-900 */
--card: #ffffff; /* white */
--card-hover: #f8fafc; /* slate-50 */
--card-column: #f8fafc; /* slate-50 - colonnes plus foncées que les cartes */
--border: #cbd5e1; /* slate-300 */
--input: #ffffff; /* white */
--primary: #0891b2; /* cyan-600 */
--primary-foreground: #ffffff; /* white */
--muted: #94a3b8; /* slate-400 - plus clair pour scrollbar */
--muted-foreground: #64748b; /* slate-500 */
--accent: #d97706; /* amber-600 */
--destructive: #dc2626; /* red-600 */
--success: #059669; /* emerald-600 */
}
@theme inline {
@@ -21,21 +52,21 @@ body {
/* Scrollbar tech style */
::-webkit-scrollbar {
width: 8px;
height: 8px;
width: 6px;
height: 6px;
}
::-webkit-scrollbar-track {
background: #1e293b; /* slate-800 */
background: var(--card);
}
::-webkit-scrollbar-thumb {
background: #475569; /* slate-600 */
background: var(--muted);
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: #06b6d4; /* cyan-500 */
background: var(--primary);
}
/* Animations tech */

View File

@@ -1,6 +1,7 @@
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";
import { ThemeProvider } from "@/contexts/ThemeContext";
const geistSans = Geist({
variable: "--font-geist-sans",
@@ -27,7 +28,9 @@ export default function RootLayout({
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
>
{children}
<ThemeProvider>
{children}
</ThemeProvider>
</body>
</html>
);

View File

@@ -81,20 +81,20 @@ export function TagsPageClient({ initialTags }: TagsPageClientProps) {
};
return (
<div className="min-h-screen bg-slate-950">
<div className="min-h-screen bg-[var(--background)]">
{/* Header simplifié */}
<div className="bg-slate-900/80 backdrop-blur-sm border-b border-slate-700/50">
<div className="bg-[var(--card)]/80 backdrop-blur-sm border-b border-[var(--border)]/50">
<div className="container mx-auto px-6 py-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-4">
{/* Bouton retour */}
<Link
href="/"
className="flex items-center justify-center w-10 h-10 rounded-lg bg-slate-800/50 border border-slate-700 hover:border-cyan-400/50 hover:bg-slate-700/50 transition-all duration-200 group"
className="flex items-center justify-center w-10 h-10 rounded-lg bg-[var(--card)] border border-[var(--border)] hover:border-[var(--primary)]/50 hover:bg-[var(--card-hover)] transition-all duration-200 group"
title="Retour au Kanban"
>
<svg
className="w-5 h-5 text-slate-400 group-hover:text-cyan-400 transition-colors"
className="w-5 h-5 text-[var(--muted-foreground)] group-hover:text-[var(--primary)] transition-colors"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
@@ -103,8 +103,8 @@ export function TagsPageClient({ initialTags }: TagsPageClientProps) {
</svg>
</Link>
<div className="w-3 h-3 bg-purple-400 rounded-full animate-pulse shadow-purple-400/50 shadow-lg"></div>
<h1 className="text-xl font-mono font-bold text-slate-100 tracking-wider">
<div className="w-3 h-3 bg-[var(--accent)] rounded-full animate-pulse shadow-[var(--accent)]/50 shadow-lg"></div>
<h1 className="text-xl font-mono font-bold text-[var(--foreground)] tracking-wider">
Tags ({filteredAndSortedTags.length})
</h1>
</div>
@@ -138,14 +138,14 @@ export function TagsPageClient({ initialTags }: TagsPageClientProps) {
{/* Messages d'état */}
{error && (
<div className="max-w-md mx-auto mb-6 bg-red-900/20 border border-red-500/30 rounded-lg p-3 text-center">
<div className="text-red-400 text-sm">Erreur : {error}</div>
<div className="max-w-md mx-auto mb-6 bg-[var(--destructive)]/20 border border-[var(--destructive)]/30 rounded-lg p-3 text-center">
<div className="text-[var(--destructive)] text-sm">Erreur : {error}</div>
</div>
)}
{loading && (
<div className="text-center py-8">
<div className="text-slate-400">Chargement...</div>
<div className="text-[var(--muted-foreground)]">Chargement...</div>
</div>
)}
@@ -153,7 +153,7 @@ export function TagsPageClient({ initialTags }: TagsPageClientProps) {
{!loading && (
<div className="max-w-6xl mx-auto">
{filteredAndSortedTags.length === 0 ? (
<div className="text-center py-12 text-slate-400">
<div className="text-center py-12 text-[var(--muted-foreground)]">
<div className="text-6xl mb-4">🏷</div>
<p className="text-lg mb-2">
{searchQuery ? 'Aucun tag trouvé' : 'Aucun tag'}
@@ -171,7 +171,7 @@ export function TagsPageClient({ initialTags }: TagsPageClientProps) {
return (
<div
key={tag.id}
className={`relative bg-slate-800/50 rounded-lg border border-slate-700 hover:border-slate-600 transition-all duration-200 p-4 group ${
className={`relative bg-[var(--card)] rounded-lg border border-[var(--border)] hover:border-[var(--border)] transition-all duration-200 p-4 group ${
isDeleting ? 'opacity-50 pointer-events-none' : ''
}`}
>
@@ -179,7 +179,7 @@ export function TagsPageClient({ initialTags }: TagsPageClientProps) {
<div className="absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity flex gap-1">
<button
onClick={() => handleEditTag(tag)}
className="p-1.5 text-slate-400 hover:text-slate-200 transition-colors rounded-lg hover:bg-slate-700/80"
className="p-1.5 text-[var(--muted-foreground)] hover:text-[var(--foreground)] transition-colors rounded-lg hover:bg-[var(--card-hover)]"
title="Modifier"
>
<svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
@@ -188,7 +188,7 @@ export function TagsPageClient({ initialTags }: TagsPageClientProps) {
</button>
<button
onClick={() => handleDeleteTag(tag)}
className="p-1.5 text-slate-400 hover:text-red-400 transition-colors rounded-lg hover:bg-red-900/20"
className="p-1.5 text-[var(--muted-foreground)] hover:text-[var(--destructive)] transition-colors rounded-lg hover:bg-[var(--destructive)]/20"
title="Supprimer"
>
<svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
@@ -204,14 +204,14 @@ export function TagsPageClient({ initialTags }: TagsPageClientProps) {
style={{ backgroundColor: tag.color }}
/>
<div className="flex-1 min-w-0">
<h3 className="text-slate-100 font-medium truncate text-sm mb-1">
<h3 className="text-[var(--foreground)] font-medium truncate text-sm mb-1">
{tag.name}
</h3>
<p className="text-slate-400 text-xs mb-2">
<p className="text-[var(--muted-foreground)] text-xs mb-2">
{usage} tâche{usage > 1 ? 's' : ''}
</p>
{tag.isPinned && (
<span className="inline-flex items-center text-purple-400 text-xs bg-purple-400/10 px-2 py-1 rounded-full">
<span className="inline-flex items-center text-[var(--accent)] text-xs bg-[var(--accent)]/10 px-2 py-1 rounded-full">
🎯 Objectif
</span>
)}

View File

@@ -0,0 +1,71 @@
'use client';
import { createContext, useContext, useEffect, useState, ReactNode } from 'react';
type Theme = 'light' | 'dark';
interface ThemeContextType {
theme: Theme;
toggleTheme: () => void;
setTheme: (theme: Theme) => void;
}
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
interface ThemeProviderProps {
children: ReactNode;
}
export function ThemeProvider({ children }: ThemeProviderProps) {
const [theme, setThemeState] = useState<Theme>('dark');
const [mounted, setMounted] = useState(false);
// Hydration safe initialization
useEffect(() => {
setMounted(true);
// Check localStorage first
const savedTheme = localStorage.getItem('theme') as Theme | null;
if (savedTheme) {
setThemeState(savedTheme);
return;
}
// Fallback to system preference
if (window.matchMedia('(prefers-color-scheme: light)').matches) {
setThemeState('light');
}
}, []);
// Apply theme class to document
useEffect(() => {
if (mounted) {
document.documentElement.className = theme;
localStorage.setItem('theme', theme);
}
}, [theme, mounted]);
const toggleTheme = () => {
setThemeState(prev => prev === 'dark' ? 'light' : 'dark');
};
const setTheme = (newTheme: Theme) => {
setThemeState(newTheme);
};
return (
<ThemeContext.Provider value={{ theme, toggleTheme, setTheme }}>
<div className={mounted ? theme : 'dark'}>
{children}
</div>
</ThemeContext.Provider>
);
}
export function useTheme() {
const context = useContext(ThemeContext);
if (context === undefined) {
throw new Error('useTheme must be used within a ThemeProvider');
}
return context;
}