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:
@@ -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 */
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
71
src/contexts/ThemeContext.tsx
Normal file
71
src/contexts/ThemeContext.tsx
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user