feat: refactor theme management and enhance color customization

- Cleaned up theme architecture by consolidating CSS variables and removing redundant theme applications, ensuring a single source of truth for theming.
- Implemented a dark mode override and improved color management using CSS variables for better customization.
- Updated various components to utilize new color variables, enhancing maintainability and visual consistency across the application.
- Added detailed tasks in TODO.md for future enhancements related to user preferences and color customization features.
This commit is contained in:
Julien Froidefond
2025-09-28 10:14:25 +02:00
parent 97770917c1
commit b5d6967fcd
21 changed files with 404 additions and 187 deletions

View File

@@ -0,0 +1,167 @@
---
alwaysApply: true
description: CSS Variables theme system best practices
---
# CSS Variables Theme System
## Core Principle: Pure CSS Variables for Theming
This project uses **CSS Variables exclusively** for theming. No Tailwind `dark:` classes or conditional CSS classes.
## ✅ Architecture Pattern
### CSS Structure
```css
:root {
/* Light theme (default values) */
--background: #f1f5f9;
--foreground: #0f172a;
--primary: #0891b2;
--success: #059669;
--destructive: #dc2626;
--accent: #d97706;
--purple: #8b5cf6;
--yellow: #eab308;
--green: #059669;
--blue: #2563eb;
--gray: #6b7280;
--gray-light: #e5e7eb;
}
.dark {
/* Dark theme (override values) */
--background: #1e293b;
--foreground: #f1f5f9;
--primary: #06b6d4;
--success: #10b981;
--destructive: #ef4444;
--accent: #f59e0b;
--purple: #8b5cf6;
--yellow: #eab308;
--green: #10b981;
--blue: #3b82f6;
--gray: #9ca3af;
--gray-light: #374151;
}
```
### Theme Application
- **Single source of truth**: [ThemeContext.tsx](mdc:src/contexts/ThemeContext.tsx) applies theme class to `document.documentElement`
- **No duplication**: Theme is applied only once, not in multiple places
- **SSR safe**: Initial theme from server-side preferences
## ✅ Component Usage Patterns
### Correct: Using CSS Variables
```tsx
// ✅ GOOD: CSS Variables in className
<div className="bg-[var(--card)] text-[var(--foreground)] border-[var(--border)]">
// ✅ GOOD: CSS Variables in style prop
<div style={{ color: 'var(--primary)', backgroundColor: 'var(--card)' }}>
// ✅ GOOD: CSS Variables with color-mix for transparency
<div style={{
backgroundColor: 'color-mix(in srgb, var(--primary) 10%, transparent)',
borderColor: 'color-mix(in srgb, var(--primary) 20%, var(--border))'
}}>
```
### ❌ Forbidden: Tailwind Dark Mode Classes
```tsx
// ❌ BAD: Tailwind dark: classes
<div className="bg-white dark:bg-gray-800 text-black dark:text-white">
// ❌ BAD: Conditional classes
<div className={theme === 'dark' ? 'bg-gray-800' : 'bg-white'}>
// ❌ BAD: Hardcoded colors
<div className="bg-red-500 text-blue-600">
```
## ✅ Color System
### Semantic Color Tokens
- `--background`: Main background color
- `--foreground`: Main text color
- `--card`: Card/panel background
- `--card-hover`: Card hover state
- `--card-column`: Column background (darker than cards)
- `--border`: Border color
- `--input`: Input field background
- `--primary`: Primary brand color
- `--primary-foreground`: Text on primary background
- `--muted`: Muted text color
- `--muted-foreground`: Secondary text color
- `--accent`: Accent color (orange/amber)
- `--destructive`: Error/danger color (red)
- `--success`: Success color (green)
- `--purple`: Purple accent
- `--yellow`: Yellow accent
- `--green`: Green accent
- `--blue`: Blue accent
- `--gray`: Gray color
- `--gray-light`: Light gray background
### Color Mixing Patterns
```css
/* Background with transparency */
background-color: color-mix(in srgb, var(--primary) 10%, transparent);
/* Border with transparency */
border-color: color-mix(in srgb, var(--primary) 20%, var(--border));
/* Text with opacity */
color: color-mix(in srgb, var(--destructive) 80%, transparent);
```
## ✅ Theme Context Usage
### ThemeProvider Setup
```tsx
// In layout.tsx
<ThemeProvider initialTheme={initialPreferences.viewPreferences.theme}>
{children}
</ThemeProvider>
```
### Component Usage
```tsx
import { useTheme } from '@/contexts/ThemeContext';
function MyComponent() {
const { theme, toggleTheme, setTheme } = useTheme();
return (
<button onClick={toggleTheme}>
Switch to {theme === 'dark' ? 'light' : 'dark'} theme
</button>
);
}
```
## ✅ Future Extensibility
This system is designed to support:
- **Custom color themes**: Easy to add new color variables
- **User preferences**: Colors can be dynamically changed
- **Theme presets**: Multiple predefined themes
- **Accessibility**: High contrast modes
## 🚨 Anti-patterns to Avoid
1. **Don't mix approaches**: Never use both CSS variables and Tailwind dark: classes
2. **Don't duplicate theme application**: Theme should be applied only in ThemeContext
3. **Don't hardcode colors**: Always use semantic color tokens
4. **Don't use conditional classes**: Use CSS variables instead
5. **Don't forget transparency**: Use `color-mix()` for semi-transparent colors
## 📁 Key Files
- [globals.css](mdc:src/app/globals.css) - CSS Variables definitions
- [ThemeContext.tsx](mdc:src/contexts/ThemeContext.tsx) - Theme management
- [UserPreferencesContext.tsx](mdc:src/contexts/UserPreferencesContext.tsx) - Preferences sync
- [layout.tsx](mdc:src/app/layout.tsx) - Theme provider setup
Remember: **CSS Variables are the single source of truth for theming. Keep it pure and consistent.**

33
TODO.md
View File

@@ -1,9 +1,40 @@
# TowerControl v2.0 - Gestionnaire de tâches moderne # TowerControl v2.0 - Gestionnaire de tâches moderne
## Idées à developper ## Idées à developper
- [x] Refacto et intégration design : mode sombre et clair sont souvent mal généré par défaut <!-- Diagnostic terminé -->
- [ ] Personnalisation : couleurs - [ ] Personnalisation : couleurs
- [ ] Optimisations Perf : requetes DB - [ ] Optimisations Perf : requetes DB
- [ ] PWA et mode offline - [ ] PWA et mode offline
---
## 🎨 **REFACTORING THÈME & PERSONNALISATION COULEURS**
### **Phase 1: Nettoyage Architecture Thème**
- [x] **Décider de la stratégie** : CSS Variables vs Tailwind Dark Mode vs Hybride <!-- CSS Variables choisi -->
- [x] **Configurer tailwind.config.js** avec `darkMode: 'class'` si nécessaire <!-- Annulé : CSS Variables pur -->
- [x] **Supprimer la double application** du thème (layout.tsx + ThemeContext + UserPreferencesContext) <!-- ThemeContext est maintenant la source unique -->
- [x] **Refactorer les CSS variables** : `:root` pour défaut, `.dark/.light` pour override <!-- Architecture CSS propre avec :root neutre -->
- [x] **Nettoyer les composants** : supprimer classes `dark:` hardcodées, utiliser uniquement CSS variables <!-- TERMINÉ : toutes les occurrences supprimées -->
- [ ] **Corriger les problèmes d'hydration** mismatch et flashs de thème
- [ ] **Créer un système de design cohérent** avec tokens de couleur
### **Phase 2: Système Couleurs Personnalisées**
- [ ] **Étendre le modèle UserPreferences** pour supporter des couleurs personnalisées
- [ ] **Créer un service de gestion** des couleurs personnalisées
- [ ] **Créer une interface de configuration** des couleurs personnalisées
- [ ] **Implémenter le système CSS** pour les couleurs personnalisées dynamiques
- [ ] **Créer un système de presets** de thèmes (Tech Dark, Corporate Light, etc.)
- [ ] **Ajouter la validation des contrastes** pour les couleurs personnalisées
- [ ] **Permettre export/import** des configurations de thème personnalisées
### **Problèmes identifiés actuellement :**
- ❌ Approche hybride incohérente (CSS Variables + Tailwind `dark:` + classes conditionnelles)
- ❌ Double application du thème (3 endroits différents)
- ❌ Pas de configuration Tailwind pour `darkMode`
- ❌ Hydration mismatch avec flashs
- ❌ CSS Variables mal optimisées (`:root` contient le thème sombre)
- ❌ Couleurs hardcodées dans certains composants
--- ---

View File

@@ -1,25 +1,7 @@
@import "tailwindcss"; @import "tailwindcss";
:root { :root {
/* Dark theme (default) */ /* Valeurs par défaut (Light theme) */
--background: #1e293b; /* slate-800 - encore plus clair */
--foreground: #f1f5f9; /* slate-100 */
--card: #334155; /* slate-700 - beaucoup plus clair pour contraste fort */
--card-hover: #475569; /* slate-600 */
--card-column: #0f172a; /* slate-900 - plus foncé que les cartes */
--border: #64748b; /* slate-500 - encore plus clair */
--input: #334155; /* slate-700 - plus clair */
--primary: #06b6d4; /* cyan-500 */
--primary-foreground: #f1f5f9; /* slate-100 */
--muted: #64748b; /* slate-500 */
--muted-foreground: #94a3b8; /* slate-400 */
--accent: #f59e0b; /* amber-500 */
--destructive: #ef4444; /* red-500 */
--success: #10b981; /* emerald-500 */
}
.light {
/* Light theme */
--background: #f1f5f9; /* slate-100 */ --background: #f1f5f9; /* slate-100 */
--foreground: #0f172a; /* slate-900 */ --foreground: #0f172a; /* slate-900 */
--card: #ffffff; /* white */ --card: #ffffff; /* white */
@@ -34,6 +16,36 @@
--accent: #d97706; /* amber-600 */ --accent: #d97706; /* amber-600 */
--destructive: #dc2626; /* red-600 */ --destructive: #dc2626; /* red-600 */
--success: #059669; /* emerald-600 */ --success: #059669; /* emerald-600 */
--purple: #8b5cf6; /* purple-500 */
--yellow: #eab308; /* yellow-500 */
--green: #059669; /* emerald-600 */
--blue: #2563eb; /* blue-600 */
--gray: #6b7280; /* gray-500 */
--gray-light: #e5e7eb; /* gray-200 */
}
.dark {
/* Dark theme override */
--background: #1e293b; /* slate-800 - encore plus clair */
--foreground: #f1f5f9; /* slate-100 */
--card: #334155; /* slate-700 - beaucoup plus clair pour contraste fort */
--card-hover: #475569; /* slate-600 */
--card-column: #0f172a; /* slate-900 - plus foncé que les cartes */
--border: #64748b; /* slate-500 - encore plus clair */
--input: #334155; /* slate-700 - plus clair */
--primary: #06b6d4; /* cyan-500 */
--primary-foreground: #f1f5f9; /* slate-100 */
--muted: #64748b; /* slate-500 */
--muted-foreground: #94a3b8; /* slate-400 */
--accent: #f59e0b; /* amber-500 */
--destructive: #ef4444; /* red-500 */
--success: #10b981; /* emerald-500 */
--purple: #8b5cf6; /* purple-500 */
--yellow: #eab308; /* yellow-500 */
--green: #10b981; /* emerald-500 */
--blue: #3b82f6; /* blue-500 */
--gray: #9ca3af; /* gray-400 */
--gray-light: #374151; /* gray-700 */
} }
@theme inline { @theme inline {
@@ -100,16 +112,16 @@ body {
.outline-card-purple { .outline-card-purple {
@apply p-2.5 rounded-lg border transition-all hover:shadow-sm hover:scale-[1.01]; @apply p-2.5 rounded-lg border transition-all hover:shadow-sm hover:scale-[1.01];
color: #8b5cf6; /* purple-500 */ color: var(--purple);
background-color: color-mix(in srgb, #8b5cf6 8%, transparent); background-color: color-mix(in srgb, var(--purple) 8%, transparent);
border-color: color-mix(in srgb, #8b5cf6 25%, var(--border)); border-color: color-mix(in srgb, var(--purple) 25%, var(--border));
} }
.outline-card-yellow { .outline-card-yellow {
@apply p-2.5 rounded-lg border transition-all hover:shadow-sm hover:scale-[1.01]; @apply p-2.5 rounded-lg border transition-all hover:shadow-sm hover:scale-[1.01];
color: #eab308; /* yellow-500 */ color: var(--yellow);
background-color: color-mix(in srgb, #eab308 8%, transparent); background-color: color-mix(in srgb, var(--yellow) 8%, transparent);
border-color: color-mix(in srgb, #eab308 25%, var(--border)); border-color: color-mix(in srgb, var(--yellow) 25%, var(--border));
} }
.outline-card-gray { .outline-card-gray {
@@ -143,9 +155,9 @@ body {
.outline-metric-purple { .outline-metric-purple {
@apply text-center p-4 rounded-lg border transition-all hover:shadow-sm hover:scale-[1.01]; @apply text-center p-4 rounded-lg border transition-all hover:shadow-sm hover:scale-[1.01];
color: #8b5cf6; /* purple-500 */ color: var(--purple);
background-color: color-mix(in srgb, #8b5cf6 8%, transparent); background-color: color-mix(in srgb, var(--purple) 8%, transparent);
border-color: color-mix(in srgb, #8b5cf6 25%, var(--border)); border-color: color-mix(in srgb, var(--purple) 25%, var(--border));
} }
.outline-metric-gray { .outline-metric-gray {
@@ -157,8 +169,8 @@ body {
/* Animations tech */ /* Animations tech */
@keyframes glow { @keyframes glow {
0%, 100% { box-shadow: 0 0 5px rgba(6, 182, 212, 0.3); } 0%, 100% { box-shadow: 0 0 5px var(--primary); }
50% { box-shadow: 0 0 20px rgba(6, 182, 212, 0.6); } 50% { box-shadow: 0 0 20px var(--primary); }
} }
.animate-glow { .animate-glow {

View File

@@ -262,7 +262,7 @@ export function JiraDashboardPageClient({ initialJiraConfig, initialAnalytics }:
{error && ( {error && (
<Card className="mb-6 border-red-500/20 bg-red-500/10"> <Card className="mb-6 border-red-500/20 bg-red-500/10">
<CardContent className="p-4"> <CardContent className="p-4">
<div className="flex items-center gap-2 text-red-600 dark:text-red-400"> <div className="flex items-center gap-2" style={{ color: 'var(--destructive)' }}>
<span></span> <span></span>
<span>{error}</span> <span>{error}</span>
</div> </div>
@@ -273,7 +273,7 @@ export function JiraDashboardPageClient({ initialJiraConfig, initialAnalytics }:
{exportError && ( {exportError && (
<Card className="mb-6 border-orange-500/20 bg-orange-500/10"> <Card className="mb-6 border-orange-500/20 bg-orange-500/10">
<CardContent className="p-4"> <CardContent className="p-4">
<div className="flex items-center gap-2 text-orange-600 dark:text-orange-400"> <div className="flex items-center gap-2" style={{ color: 'var(--accent)' }}>
<span></span> <span></span>
<span>Erreur d&apos;export: {exportError}</span> <span>Erreur d&apos;export: {exportError}</span>
</div> </div>

View File

@@ -30,7 +30,7 @@ export default async function RootLayout({
const initialPreferences = await userPreferencesService.getAllPreferences(); const initialPreferences = await userPreferencesService.getAllPreferences();
return ( return (
<html lang="en" className={initialPreferences.viewPreferences.theme}> <html lang="fr">
<body <body
className={`${geistSans.variable} ${geistMono.variable} antialiased`} className={`${geistSans.variable} ${geistMono.variable} antialiased`}
> >

View File

@@ -88,7 +88,7 @@ export function BackupTimelineChart({ stats = [], className = '' }: BackupTimeli
<div className={` <div className={`
relative h-8 rounded border-2 transition-all duration-200 cursor-pointer flex items-center justify-center text-xs font-medium relative h-8 rounded border-2 transition-all duration-200 cursor-pointer flex items-center justify-center text-xs font-medium
${stat.total === 0 ${stat.total === 0
? 'bg-gray-50 dark:bg-gray-800 border-gray-200 dark:border-gray-700 text-gray-400' ? 'border-[var(--border)] text-[var(--muted-foreground)]'
: 'border-transparent' : 'border-transparent'
} }
`}> `}>
@@ -152,58 +152,58 @@ export function BackupTimelineChart({ stats = [], className = '' }: BackupTimeli
</div> </div>
{/* Légende claire */} {/* Légende claire */}
<div className="mb-6 p-3 bg-gray-50 dark:bg-gray-800/50 rounded-lg"> <div className="mb-6 p-3 rounded-lg" style={{ backgroundColor: 'var(--card-hover)' }}>
<h4 className="text-sm font-medium mb-3 text-gray-700 dark:text-gray-300">Légende</h4> <h4 className="text-sm font-medium mb-3 text-[var(--foreground)]">Légende</h4>
<div className="grid grid-cols-2 gap-3 text-sm"> <div className="grid grid-cols-2 gap-3 text-sm">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div className="w-6 h-6 bg-blue-500 rounded flex items-center justify-center text-white text-xs font-bold">15</div> <div className="w-6 h-6 bg-blue-500 rounded flex items-center justify-center text-white text-xs font-bold">15</div>
<span className="text-gray-700 dark:text-gray-300">Manuel seul</span> <span className="text-[var(--foreground)]">Manuel seul</span>
</div> </div>
</div> </div>
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div className="w-6 h-6 bg-green-500 rounded flex items-center justify-center text-white text-xs font-bold">15</div> <div className="w-6 h-6 bg-green-500 rounded flex items-center justify-center text-white text-xs font-bold">15</div>
<span className="text-gray-700 dark:text-gray-300">Auto seul</span> <span className="text-[var(--foreground)]">Auto seul</span>
</div> </div>
</div> </div>
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div className="w-6 h-6 bg-gradient-to-br from-blue-500 to-green-500 rounded flex items-center justify-center text-white text-xs font-bold">15</div> <div className="w-6 h-6 bg-gradient-to-br from-blue-500 to-green-500 rounded flex items-center justify-center text-white text-xs font-bold">15</div>
<span className="text-gray-700 dark:text-gray-300">Manuel + Auto</span> <span className="text-[var(--foreground)]">Manuel + Auto</span>
</div> </div>
</div> </div>
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div className="w-6 h-6 bg-gray-200 dark:bg-gray-700 border-2 border-gray-300 dark:border-gray-600 rounded flex items-center justify-center text-gray-500 text-xs">15</div> <div className="w-6 h-6 border-2 rounded flex items-center justify-center text-xs" style={{ backgroundColor: 'var(--gray-light)', borderColor: 'var(--border)', color: 'var(--muted-foreground)' }}>15</div>
<span className="text-gray-700 dark:text-gray-300">Aucune</span> <span className="text-[var(--foreground)]">Aucune</span>
</div> </div>
</div> </div>
</div> </div>
<div className="mt-3 text-xs text-gray-600 dark:text-gray-400"> <div className="mt-3 text-xs text-[var(--muted-foreground)]">
💡 Le badge orange indique le nombre total quand &gt; 1 💡 Le badge orange indique le nombre total quand &gt; 1
</div> </div>
</div> </div>
{/* Statistiques résumées */} {/* Statistiques résumées */}
<div className="grid grid-cols-3 gap-3 text-center"> <div className="grid grid-cols-3 gap-3 text-center">
<div className="p-3 bg-blue-50 dark:bg-blue-900/20 rounded-lg"> <div className="p-3 rounded-lg" style={{ backgroundColor: 'color-mix(in srgb, var(--blue) 10%, transparent)' }}>
<div className="text-xl font-bold text-blue-600"> <div className="text-xl font-bold" style={{ color: 'var(--blue)' }}>
{safeStats.reduce((sum, s) => sum + s.manual, 0)} {safeStats.reduce((sum, s) => sum + s.manual, 0)}
</div> </div>
<div className="text-xs text-blue-600 font-medium">Manuelles</div> <div className="text-xs font-medium" style={{ color: 'var(--blue)' }}>Manuelles</div>
</div> </div>
<div className="p-3 bg-green-50 dark:bg-green-900/20 rounded-lg"> <div className="p-3 rounded-lg" style={{ backgroundColor: 'color-mix(in srgb, var(--green) 10%, transparent)' }}>
<div className="text-xl font-bold text-green-600"> <div className="text-xl font-bold" style={{ color: 'var(--green)' }}>
{safeStats.reduce((sum, s) => sum + s.automatic, 0)} {safeStats.reduce((sum, s) => sum + s.automatic, 0)}
</div> </div>
<div className="text-xs text-green-600 font-medium">Automatiques</div> <div className="text-xs font-medium" style={{ color: 'var(--green)' }}>Automatiques</div>
</div> </div>
<div className="p-3 bg-purple-50 dark:bg-purple-900/20 rounded-lg"> <div className="p-3 rounded-lg" style={{ backgroundColor: 'color-mix(in srgb, var(--purple) 10%, transparent)' }}>
<div className="text-xl font-bold text-purple-600"> <div className="text-xl font-bold" style={{ color: 'var(--purple)' }}>
{safeStats.reduce((sum, s) => sum + s.total, 0)} {safeStats.reduce((sum, s) => sum + s.total, 0)}
</div> </div>
<div className="text-xs text-purple-600 font-medium">Total</div> <div className="text-xs font-medium" style={{ color: 'var(--purple)' }}>Total</div>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -19,13 +19,13 @@ export function WeeklyActivityHeatmap({ data, className }: WeeklyActivityHeatmap
}; };
// Obtenir la couleur basée sur l'intensité // Obtenir la couleur basée sur l'intensité
const getColorClass = (intensity: number) => { const getColorStyle = (intensity: number) => {
if (intensity === 0) return 'bg-gray-100 dark:bg-gray-800'; if (intensity === 0) return { backgroundColor: 'var(--gray-light)' };
if (intensity < 0.2) return 'bg-green-100 dark:bg-green-900/30'; if (intensity < 0.2) return { backgroundColor: 'color-mix(in srgb, var(--green) 20%, transparent)' };
if (intensity < 0.4) return 'bg-green-200 dark:bg-green-800/50'; if (intensity < 0.4) return { backgroundColor: 'color-mix(in srgb, var(--green) 40%, transparent)' };
if (intensity < 0.6) return 'bg-green-300 dark:bg-green-700/70'; if (intensity < 0.6) return { backgroundColor: 'color-mix(in srgb, var(--green) 60%, transparent)' };
if (intensity < 0.8) return 'bg-green-400 dark:bg-green-600/80'; if (intensity < 0.8) return { backgroundColor: 'color-mix(in srgb, var(--green) 80%, transparent)' };
return 'bg-green-500 dark:bg-green-500'; return { backgroundColor: 'var(--green)' };
}; };
return ( return (
@@ -46,14 +46,15 @@ export function WeeklyActivityHeatmap({ data, className }: WeeklyActivityHeatmap
<div className="flex gap-1"> <div className="flex gap-1">
{data.map((day, index) => { {data.map((day, index) => {
const intensity = getIntensity(day); const intensity = getIntensity(day);
const colorClass = getColorClass(intensity); const colorStyle = getColorStyle(intensity);
const totalActivity = day.completed + day.newTasks; const totalActivity = day.completed + day.newTasks;
return ( return (
<div key={index} className="text-center"> <div key={index} className="text-center">
{/* Carré de couleur */} {/* Carré de couleur */}
<div <div
className={`w-8 h-8 rounded ${colorClass} border border-[var(--border)] flex items-center justify-center transition-all hover:scale-110 cursor-help group relative`} className="w-8 h-8 rounded border border-[var(--border)] flex items-center justify-center transition-all hover:scale-110 cursor-help group relative"
style={colorStyle}
title={`${day.dayName}: ${totalActivity} activités (${day.completed} complétées, ${day.newTasks} créées)`} title={`${day.dayName}: ${totalActivity} activités (${day.completed} complétées, ${day.newTasks} créées)`}
> >
{/* Tooltip au hover */} {/* Tooltip au hover */}
@@ -87,12 +88,12 @@ export function WeeklyActivityHeatmap({ data, className }: WeeklyActivityHeatmap
<div className="flex items-center justify-center gap-2 text-xs text-[var(--muted-foreground)]"> <div className="flex items-center justify-center gap-2 text-xs text-[var(--muted-foreground)]">
<span>Moins</span> <span>Moins</span>
<div className="flex gap-1"> <div className="flex gap-1">
<div className="w-3 h-3 bg-gray-100 dark:bg-gray-800 border border-[var(--border)] rounded"></div> <div className="w-3 h-3 border border-[var(--border)] rounded" style={{ backgroundColor: 'var(--gray-light)' }}></div>
<div className="w-3 h-3 bg-green-100 dark:bg-green-900/30 border border-[var(--border)] rounded"></div> <div className="w-3 h-3 border border-[var(--border)] rounded" style={{ backgroundColor: 'color-mix(in srgb, var(--green) 20%, transparent)' }}></div>
<div className="w-3 h-3 bg-green-200 dark:bg-green-800/50 border border-[var(--border)] rounded"></div> <div className="w-3 h-3 border border-[var(--border)] rounded" style={{ backgroundColor: 'color-mix(in srgb, var(--green) 40%, transparent)' }}></div>
<div className="w-3 h-3 bg-green-300 dark:bg-green-700/70 border border-[var(--border)] rounded"></div> <div className="w-3 h-3 border border-[var(--border)] rounded" style={{ backgroundColor: 'color-mix(in srgb, var(--green) 60%, transparent)' }}></div>
<div className="w-3 h-3 bg-green-400 dark:bg-green-600/80 border border-[var(--border)] rounded"></div> <div className="w-3 h-3 border border-[var(--border)] rounded" style={{ backgroundColor: 'color-mix(in srgb, var(--green) 80%, transparent)' }}></div>
<div className="w-3 h-3 bg-green-500 dark:bg-green-500 border border-[var(--border)] rounded"></div> <div className="w-3 h-3 border border-[var(--border)] rounded" style={{ backgroundColor: 'var(--green)' }}></div>
</div> </div>
<span>Plus</span> <span>Plus</span>
</div> </div>

View File

@@ -27,7 +27,7 @@ export function CriticalDeadlinesCard({ overdue, critical, warning }: CriticalDe
return { return {
icon: '🔴', icon: '🔴',
text: task.daysRemaining === -1 ? 'En retard de 1 jour' : `En retard de ${Math.abs(task.daysRemaining)} jours`, text: task.daysRemaining === -1 ? 'En retard de 1 jour' : `En retard de ${Math.abs(task.daysRemaining)} jours`,
style: 'text-red-700 bg-red-50/40 border-red-200/60 dark:bg-red-950/20 dark:border-red-800/40 dark:text-red-300' style: 'border-[var(--destructive)]/60'
}; };
} else if (task.urgencyLevel === 'critical') { } else if (task.urgencyLevel === 'critical') {
return { return {
@@ -35,13 +35,13 @@ export function CriticalDeadlinesCard({ overdue, critical, warning }: CriticalDe
text: task.daysRemaining === 0 ? 'Échéance aujourd\'hui' : text: task.daysRemaining === 0 ? 'Échéance aujourd\'hui' :
task.daysRemaining === 1 ? 'Échéance demain' : task.daysRemaining === 1 ? 'Échéance demain' :
`Dans ${task.daysRemaining} jours`, `Dans ${task.daysRemaining} jours`,
style: 'text-orange-700 bg-orange-50/40 border-orange-200/60 dark:bg-orange-950/20 dark:border-orange-800/40 dark:text-orange-300' style: 'border-[var(--accent)]/60'
}; };
} else { } else {
return { return {
icon: '🟡', icon: '🟡',
text: `Dans ${task.daysRemaining} jours`, text: `Dans ${task.daysRemaining} jours`,
style: 'text-yellow-700 bg-yellow-50/40 border-yellow-200/60 dark:bg-yellow-950/20 dark:border-yellow-800/40 dark:text-yellow-300' style: 'border-[var(--yellow)]/60'
}; };
} }
}; };
@@ -71,7 +71,7 @@ export function CriticalDeadlinesCard({ overdue, critical, warning }: CriticalDe
<h3 className="text-lg font-semibold mb-4">Tâches Urgentes</h3> <h3 className="text-lg font-semibold mb-4">Tâches Urgentes</h3>
<div className="text-center py-8"> <div className="text-center py-8">
<div className="text-4xl mb-2">🎉</div> <div className="text-4xl mb-2">🎉</div>
<h4 className="text-lg font-medium text-green-600 dark:text-green-400 mb-2">Excellent !</h4> <h4 className="text-lg font-medium mb-2" style={{ color: 'var(--green)' }}>Excellent !</h4>
<p className="text-sm text-[var(--muted-foreground)]"> <p className="text-sm text-[var(--muted-foreground)]">
Aucune tâche urgente ou critique Aucune tâche urgente ou critique
</p> </p>
@@ -89,7 +89,7 @@ export function CriticalDeadlinesCard({ overdue, critical, warning }: CriticalDe
</div> </div>
</div> </div>
<div className="space-y-2 max-h-40 overflow-y-auto scrollbar-thin scrollbar-thumb-gray-300 dark:scrollbar-thumb-gray-600 scrollbar-track-transparent pr-2"> <div className="space-y-2 max-h-40 overflow-y-auto scrollbar-thin scrollbar-track-transparent pr-2" style={{ scrollbarColor: 'var(--muted) transparent' }}>
{urgentTasks.map((task) => { {urgentTasks.map((task) => {
const urgencyStyle = getUrgencyStyle(task); const urgencyStyle = getUrgencyStyle(task);
@@ -157,17 +157,17 @@ export function CriticalDeadlinesCard({ overdue, critical, warning }: CriticalDe
<div className="pt-3 border-t border-[var(--border)] mt-4"> <div className="pt-3 border-t border-[var(--border)] mt-4">
<div className="flex flex-wrap gap-3 text-xs text-[var(--muted-foreground)] justify-center"> <div className="flex flex-wrap gap-3 text-xs text-[var(--muted-foreground)] justify-center">
{overdue.length > 0 && ( {overdue.length > 0 && (
<span className="text-red-600/80 dark:text-red-400/80 font-medium"> <span className="font-medium" style={{ color: 'var(--destructive)' }}>
{overdue.length} en retard {overdue.length} en retard
</span> </span>
)} )}
{critical.length > 0 && ( {critical.length > 0 && (
<span className="text-orange-600/80 dark:text-orange-400/80 font-medium"> <span className="font-medium" style={{ color: 'var(--accent)' }}>
{critical.length} critique{critical.length > 1 ? 's' : ''} {critical.length} critique{critical.length > 1 ? 's' : ''}
</span> </span>
)} )}
{warning.length > 0 && ( {warning.length > 0 && (
<span className="text-yellow-600/80 dark:text-yellow-400/80 font-medium"> <span className="font-medium" style={{ color: 'var(--yellow)' }}>
{warning.length} attention {warning.length} attention
</span> </span>
)} )}

View File

@@ -22,21 +22,21 @@ export function DeadlineRiskCard({ metrics }: DeadlineRiskCardProps) {
const getRiskColor = (level: string) => { const getRiskColor = (level: string) => {
switch (level) { switch (level) {
case 'critical': return 'text-red-600 dark:text-red-400'; case 'critical': return { color: 'var(--destructive)' };
case 'high': return 'text-orange-600 dark:text-orange-400'; case 'high': return { color: 'var(--accent)' };
case 'medium': return 'text-yellow-600 dark:text-yellow-400'; case 'medium': return { color: 'var(--yellow)' };
case 'low': return 'text-green-600 dark:text-green-400'; case 'low': return { color: 'var(--green)' };
default: return 'text-gray-600 dark:text-gray-400'; default: return { color: 'var(--muted-foreground)' };
} }
}; };
const getRiskBgColor = (level: string) => { const getRiskBgColor = (level: string) => {
switch (level) { switch (level) {
case 'critical': return 'bg-red-50/30 border-red-200/50 dark:bg-red-950/20 dark:border-red-800/30'; case 'critical': return { backgroundColor: 'color-mix(in srgb, var(--destructive) 10%, transparent)', borderColor: 'color-mix(in srgb, var(--destructive) 30%, var(--border))' };
case 'high': return 'bg-orange-50/30 border-orange-200/50 dark:bg-orange-950/20 dark:border-orange-800/30'; case 'high': return { backgroundColor: 'color-mix(in srgb, var(--accent) 10%, transparent)', borderColor: 'color-mix(in srgb, var(--accent) 30%, var(--border))' };
case 'medium': return 'bg-yellow-50/30 border-yellow-200/50 dark:bg-yellow-950/20 dark:border-yellow-800/30'; case 'medium': return { backgroundColor: 'color-mix(in srgb, var(--yellow) 10%, transparent)', borderColor: 'color-mix(in srgb, var(--yellow) 30%, var(--border))' };
case 'low': return 'bg-green-50/30 border-green-200/50 dark:bg-green-950/20 dark:border-green-800/30'; case 'low': return { backgroundColor: 'color-mix(in srgb, var(--green) 10%, transparent)', borderColor: 'color-mix(in srgb, var(--green) 30%, var(--border))' };
default: return 'bg-gray-50/30 border-gray-200/50 dark:bg-gray-950/20 dark:border-gray-800/30'; default: return { backgroundColor: 'color-mix(in srgb, var(--muted) 10%, transparent)', borderColor: 'color-mix(in srgb, var(--muted) 30%, var(--border))' };
} }
}; };
@@ -47,7 +47,7 @@ export function DeadlineRiskCard({ metrics }: DeadlineRiskCardProps) {
<span className="text-2xl">{getRiskIcon(riskAnalysis.riskLevel)}</span> <span className="text-2xl">{getRiskIcon(riskAnalysis.riskLevel)}</span>
<h3 className="text-lg font-semibold">Niveau de Risque</h3> <h3 className="text-lg font-semibold">Niveau de Risque</h3>
</div> </div>
<div className={`text-3xl font-bold ${getRiskColor(riskAnalysis.riskLevel)}`}> <div className="text-3xl font-bold" style={getRiskColor(riskAnalysis.riskLevel)}>
{riskAnalysis.riskScore} {riskAnalysis.riskScore}
</div> </div>
</div> </div>
@@ -69,11 +69,11 @@ export function DeadlineRiskCard({ metrics }: DeadlineRiskCardProps) {
<div className="grid grid-cols-2 gap-2 text-sm"> <div className="grid grid-cols-2 gap-2 text-sm">
<div className="flex justify-between"> <div className="flex justify-between">
<span className="text-[var(--muted-foreground)]">En retard:</span> <span className="text-[var(--muted-foreground)]">En retard:</span>
<span className="font-medium text-red-600/80 dark:text-red-400/80">{metrics.summary.overdueCount}</span> <span className="font-medium" style={{ color: 'var(--destructive)' }}>{metrics.summary.overdueCount}</span>
</div> </div>
<div className="flex justify-between"> <div className="flex justify-between">
<span className="text-[var(--muted-foreground)]">Critique:</span> <span className="text-[var(--muted-foreground)]">Critique:</span>
<span className="font-medium text-orange-600/80 dark:text-orange-400/80">{metrics.summary.criticalCount}</span> <span className="font-medium" style={{ color: 'var(--accent)' }}>{metrics.summary.criticalCount}</span>
</div> </div>
</div> </div>

View File

@@ -15,29 +15,29 @@ export function DeadlineSummaryCard({ metrics }: DeadlineSummaryCardProps) {
label: 'En retard', label: 'En retard',
count: summary.overdueCount, count: summary.overdueCount,
icon: '⏰', icon: '⏰',
color: 'text-red-600 dark:text-red-400', color: 'var(--destructive)',
bgColor: 'bg-red-100/50 dark:bg-red-900/30' bgColor: 'color-mix(in srgb, var(--destructive) 10%, transparent)'
}, },
{ {
label: 'Critique (0-2j)', label: 'Critique (0-2j)',
count: summary.criticalCount, count: summary.criticalCount,
icon: '🚨', icon: '🚨',
color: 'text-orange-600 dark:text-orange-400', color: 'var(--accent)',
bgColor: 'bg-orange-100/50 dark:bg-orange-900/30' bgColor: 'color-mix(in srgb, var(--accent) 10%, transparent)'
}, },
{ {
label: 'Attention (3-7j)', label: 'Attention (3-7j)',
count: summary.warningCount, count: summary.warningCount,
icon: '⚠️', icon: '⚠️',
color: 'text-yellow-600 dark:text-yellow-400', color: 'var(--yellow)',
bgColor: 'bg-yellow-100/50 dark:bg-yellow-900/30' bgColor: 'color-mix(in srgb, var(--yellow) 10%, transparent)'
}, },
{ {
label: 'À venir (8-14j)', label: 'À venir (8-14j)',
count: summary.upcomingCount, count: summary.upcomingCount,
icon: '📅', icon: '📅',
color: 'text-blue-600 dark:text-blue-400', color: 'var(--blue)',
bgColor: 'bg-blue-100/50 dark:bg-blue-900/30' bgColor: 'color-mix(in srgb, var(--blue) 10%, transparent)'
} }
]; ];
@@ -54,12 +54,12 @@ export function DeadlineSummaryCard({ metrics }: DeadlineSummaryCardProps) {
{summaryItems.map((item, index) => ( {summaryItems.map((item, index) => (
<div key={index} className="flex items-center justify-between"> <div key={index} className="flex items-center justify-between">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className={`w-8 h-8 ${item.bgColor} rounded-full flex items-center justify-center text-sm`}> <div className="w-8 h-8 rounded-full flex items-center justify-center text-sm" style={{ backgroundColor: item.bgColor }}>
{item.icon} {item.icon}
</div> </div>
<span className="text-sm font-medium">{item.label}</span> <span className="text-sm font-medium">{item.label}</span>
</div> </div>
<div className={`text-lg font-bold ${item.color}`}> <div className="text-lg font-bold" style={{ color: item.color }}>
{item.count} {item.count}
</div> </div>
</div> </div>
@@ -73,7 +73,7 @@ export function DeadlineSummaryCard({ metrics }: DeadlineSummaryCardProps) {
{summary.totalWithDeadlines - summary.overdueCount - summary.criticalCount}/{summary.totalWithDeadlines} {summary.totalWithDeadlines - summary.overdueCount - summary.criticalCount}/{summary.totalWithDeadlines}
</span> </span>
</div> </div>
<div className="w-full bg-gray-200/50 dark:bg-gray-700/50 rounded-full h-2 mt-2"> <div className="w-full rounded-full h-2 mt-2" style={{ backgroundColor: 'var(--gray-light)' }}>
<div <div
className="bg-green-500/80 h-2 rounded-full transition-all duration-300" className="bg-green-500/80 h-2 rounded-full transition-all duration-300"
style={{ style={{

View File

@@ -131,19 +131,19 @@ export function BurndownChart({ sprintHistory, className }: BurndownChartProps)
{/* Légende visuelle */} {/* Légende visuelle */}
<div className="mb-4 flex justify-center gap-6 text-sm"> <div className="mb-4 flex justify-center gap-6 text-sm">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div className="w-4 h-0.5 bg-green-600 dark:bg-green-500 border-dashed border-t-2 border-green-600 dark:border-green-500"></div> <div className="w-4 h-0.5 border-dashed border-t-2" style={{ backgroundColor: 'var(--green)', borderColor: 'var(--green)' }}></div>
<span className="text-green-600 dark:text-green-500">Idéal</span> <span style={{ color: 'var(--green)' }}>Idéal</span>
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div className="w-4 h-0.5 bg-blue-600 dark:bg-blue-500"></div> <div className="w-4 h-0.5" style={{ backgroundColor: 'var(--blue)' }}></div>
<span className="text-blue-600 dark:text-blue-500">Réel</span> <span style={{ color: 'var(--blue)' }}>Réel</span>
</div> </div>
</div> </div>
{/* Métriques */} {/* Métriques */}
<div className="grid grid-cols-3 gap-4 text-center"> <div className="grid grid-cols-3 gap-4 text-center">
<div> <div>
<div className="text-sm font-medium text-green-500"> <div className="text-sm font-medium" style={{ color: 'var(--green)' }}>
{currentSprint.plannedPoints} {currentSprint.plannedPoints}
</div> </div>
<div className="text-xs text-[var(--muted-foreground)]"> <div className="text-xs text-[var(--muted-foreground)]">
@@ -151,7 +151,7 @@ export function BurndownChart({ sprintHistory, className }: BurndownChartProps)
</div> </div>
</div> </div>
<div> <div>
<div className="text-sm font-medium text-blue-500"> <div className="text-sm font-medium" style={{ color: 'var(--blue)' }}>
{currentSprint.completedPoints} {currentSprint.completedPoints}
</div> </div>
<div className="text-xs text-[var(--muted-foreground)]"> <div className="text-xs text-[var(--muted-foreground)]">

View File

@@ -80,7 +80,7 @@ export function CollaborationMatrix({ analytics, className }: CollaborationMatri
if (!isClient) { if (!isClient) {
return ( return (
<div className={className}> <div className={className}>
<div className="animate-pulse bg-gray-200 dark:bg-gray-700 rounded-lg h-96" /> <div className="animate-pulse rounded-lg h-96" style={{ backgroundColor: 'var(--gray-light)' }} />
</div> </div>
); );
} }
@@ -96,9 +96,9 @@ const mostIsolated = collaborationData.reduce((max, current) =>
// Couleur d'intensité // Couleur d'intensité
const getIntensityColor = (intensity: 'low' | 'medium' | 'high') => { const getIntensityColor = (intensity: 'low' | 'medium' | 'high') => {
switch (intensity) { switch (intensity) {
case 'high': return 'bg-green-600 dark:bg-green-500'; case 'high': return { backgroundColor: 'var(--green)' };
case 'medium': return 'bg-yellow-600 dark:bg-yellow-500'; case 'medium': return { backgroundColor: 'var(--yellow)' };
case 'low': return 'bg-gray-500 dark:bg-gray-400'; case 'low': return { backgroundColor: 'var(--gray)' };
} }
}; };
@@ -117,10 +117,13 @@ const mostIsolated = collaborationData.reduce((max, current) =>
<span className="text-xs text-[var(--muted-foreground)]"> <span className="text-xs text-[var(--muted-foreground)]">
Score: {person.collaborationScore} Score: {person.collaborationScore}
</span> </span>
<div className={`w-3 h-3 rounded-full ${ <div
person.isolation < 30 ? 'bg-green-600 dark:bg-green-500' : className="w-3 h-3 rounded-full"
person.isolation < 60 ? 'bg-yellow-600 dark:bg-yellow-500' : 'bg-red-600 dark:bg-red-500' style={{
}`} /> backgroundColor: person.isolation < 30 ? 'var(--green)' :
person.isolation < 60 ? 'var(--yellow)' : 'var(--destructive)'
}}
/>
</div> </div>
</div> </div>
<div className="space-y-1"> <div className="space-y-1">
@@ -132,7 +135,7 @@ const mostIsolated = collaborationData.reduce((max, current) =>
</span> </span>
<div className="flex items-center gap-2 flex-shrink-0"> <div className="flex items-center gap-2 flex-shrink-0">
<span>{dep.sharedTickets} tickets</span> <span>{dep.sharedTickets} tickets</span>
<div className={`w-2 h-2 rounded-full ${getIntensityColor(dep.intensity)}`} /> <div className="w-2 h-2 rounded-full" style={getIntensityColor(dep.intensity)} />
</div> </div>
</div> </div>
)) ))
@@ -159,11 +162,11 @@ const mostIsolated = collaborationData.reduce((max, current) =>
const ranges = [[0, 30], [30, 50], [50, 70], [70, 100]]; const ranges = [[0, 30], [30, 50], [50, 70], [70, 100]];
const [min, max] = ranges[index]; const [min, max] = ranges[index];
const count = collaborationData.filter(d => d.isolation >= min && d.isolation < max).length; const count = collaborationData.filter(d => d.isolation >= min && d.isolation < max).length;
const colors = ['bg-green-600 dark:bg-green-500', 'bg-blue-600 dark:bg-blue-500', 'bg-yellow-600 dark:bg-yellow-500', 'bg-red-600 dark:bg-red-500']; const colors = ['var(--green)', 'var(--blue)', 'var(--yellow)', 'var(--destructive)'];
return ( return (
<div key={level} className="flex items-center gap-2 text-xs"> <div key={level} className="flex items-center gap-2 text-xs">
<div className={`w-3 h-3 rounded-sm ${colors[index]}`} /> <div className="w-3 h-3 rounded-sm" style={{ backgroundColor: colors[index] }} />
<span className="flex-1 truncate">{level}</span> <span className="flex-1 truncate">{level}</span>
<span className="font-mono text-xs">{count}</span> <span className="font-mono text-xs">{count}</span>
</div> </div>
@@ -203,7 +206,7 @@ const mostIsolated = collaborationData.reduce((max, current) =>
{ intensity: 'low' as const, label: 'Faible' } { intensity: 'low' as const, label: 'Faible' }
].map(item => ( ].map(item => (
<div key={item.intensity} className="flex items-center gap-2 text-xs"> <div key={item.intensity} className="flex items-center gap-2 text-xs">
<div className={`w-2 h-2 rounded-full ${getIntensityColor(item.intensity)}`} /> <div className="w-2 h-2 rounded-full" style={getIntensityColor(item.intensity)} />
<span>{item.label}</span> <span>{item.label}</span>
</div> </div>
))} ))}
@@ -257,25 +260,25 @@ const mostIsolated = collaborationData.reduce((max, current) =>
<h4 className="text-sm font-medium mb-2">Recommandations d&apos;équipe</h4> <h4 className="text-sm font-medium mb-2">Recommandations d&apos;équipe</h4>
<div className="space-y-2 text-sm"> <div className="space-y-2 text-sm">
{avgIsolation > 60 && ( {avgIsolation > 60 && (
<div className="flex items-center gap-2 text-red-600 dark:text-red-400"> <div className="flex items-center gap-2" style={{ color: 'var(--destructive)' }}>
<span></span> <span></span>
<span>Isolation élevée - Encourager le pair programming et les reviews croisées</span> <span>Isolation élevée - Encourager le pair programming et les reviews croisées</span>
</div> </div>
)} )}
{avgIsolation < 30 && ( {avgIsolation < 30 && (
<div className="flex items-center gap-2 text-green-600 dark:text-green-400"> <div className="flex items-center gap-2" style={{ color: 'var(--green)' }}>
<span></span> <span></span>
<span>Excellente collaboration - L&apos;équipe travaille bien ensemble</span> <span>Excellente collaboration - L&apos;équipe travaille bien ensemble</span>
</div> </div>
)} )}
{mostIsolated && mostIsolated.isolation > 80 && ( {mostIsolated && mostIsolated.isolation > 80 && (
<div className="flex items-center gap-2 text-orange-600 dark:text-orange-400"> <div className="flex items-center gap-2" style={{ color: 'var(--accent)' }}>
<span>👥</span> <span>👥</span>
<span>Attention à {mostIsolated.displayName} - Considérer du mentoring ou du binômage</span> <span>Attention à {mostIsolated.displayName} - Considérer du mentoring ou du binômage</span>
</div> </div>
)} )}
{collaborationData.filter(d => d.dependencies.length === 0).length > 0 && ( {collaborationData.filter(d => d.dependencies.length === 0).length > 0 && (
<div className="flex items-center gap-2 text-blue-600 dark:text-blue-400"> <div className="flex items-center gap-2" style={{ color: 'var(--blue)' }}>
<span>🔗</span> <span>🔗</span>
<span>Quelques membres travaillent en silo - Organiser des sessions de partage</span> <span>Quelques membres travaillent en silo - Organiser des sessions de partage</span>
</div> </div>

View File

@@ -150,7 +150,7 @@ export function JiraSync({ onSyncComplete, className = "" }: JiraSyncProps) {
<CardHeader className="pb-3"> <CardHeader className="pb-3">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="w-2 h-2 rounded-full bg-blue-500 dark:bg-blue-400 animate-pulse"></div> <div className="w-2 h-2 rounded-full animate-pulse" style={{ backgroundColor: 'var(--blue)' }}></div>
<h3 className="font-mono text-sm font-bold text-blue-400 uppercase tracking-wider"> <h3 className="font-mono text-sm font-bold text-blue-400 uppercase tracking-wider">
JIRA SYNC JIRA SYNC
</h3> </h3>

View File

@@ -205,31 +205,31 @@ export function PredictabilityMetrics({ sprintHistory, className }: Predictabili
<h4 className="text-sm font-medium mb-2">Analyse de predictabilité</h4> <h4 className="text-sm font-medium mb-2">Analyse de predictabilité</h4>
<div className="space-y-2 text-sm"> <div className="space-y-2 text-sm">
{averageAccuracy > 80 && ( {averageAccuracy > 80 && (
<div className="flex items-center gap-2 text-green-600 dark:text-green-400"> <div className="flex items-center gap-2" style={{ color: 'var(--green)' }}>
<span></span> <span></span>
<span>Excellente predictabilité - L&apos;équipe estime bien sa capacité</span> <span>Excellente predictabilité - L&apos;équipe estime bien sa capacité</span>
</div> </div>
)} )}
{averageAccuracy < 60 && ( {averageAccuracy < 60 && (
<div className="flex items-center gap-2 text-red-600 dark:text-red-400"> <div className="flex items-center gap-2" style={{ color: 'var(--destructive)' }}>
<span></span> <span></span>
<span>Predictabilité faible - Revoir les méthodes d&apos;estimation</span> <span>Predictabilité faible - Revoir les méthodes d&apos;estimation</span>
</div> </div>
)} )}
{averageVariance > 25 && ( {averageVariance > 25 && (
<div className="flex items-center gap-2 text-orange-600 dark:text-orange-400"> <div className="flex items-center gap-2" style={{ color: 'var(--accent)' }}>
<span>📊</span> <span>📊</span>
<span>Variance élevée - Considérer des sprints plus courts ou un meilleur découpage</span> <span>Variance élevée - Considérer des sprints plus courts ou un meilleur découpage</span>
</div> </div>
)} )}
{trend > 10 && ( {trend > 10 && (
<div className="flex items-center gap-2 text-green-600 dark:text-green-400"> <div className="flex items-center gap-2" style={{ color: 'var(--green)' }}>
<span>📈</span> <span>📈</span>
<span>Tendance positive - L&apos;équipe s&apos;améliore dans ses estimations</span> <span>Tendance positive - L&apos;équipe s&apos;améliore dans ses estimations</span>
</div> </div>
)} )}
{trend < -10 && ( {trend < -10 && (
<div className="flex items-center gap-2 text-red-600 dark:text-red-400"> <div className="flex items-center gap-2" style={{ color: 'var(--destructive)' }}>
<span>📉</span> <span>📉</span>
<span>Tendance négative - Attention aux changements récents (équipe, processus)</span> <span>Tendance négative - Attention aux changements récents (équipe, processus)</span>
</div> </div>

View File

@@ -190,19 +190,19 @@ export function QualityMetrics({ analytics, className }: QualityMetricsProps) {
<h4 className="text-sm font-medium mb-2">Analyse qualité</h4> <h4 className="text-sm font-medium mb-2">Analyse qualité</h4>
<div className="space-y-2 text-sm"> <div className="space-y-2 text-sm">
{bugRatio > 25 && ( {bugRatio > 25 && (
<div className="flex items-center gap-2 text-red-600 dark:text-red-400"> <div className="flex items-center gap-2" style={{ color: 'var(--destructive)' }}>
<span></span> <span></span>
<span>Ratio de bugs élevé ({bugRatio}%) - Attention à la dette technique</span> <span>Ratio de bugs élevé ({bugRatio}%) - Attention à la dette technique</span>
</div> </div>
)} )}
{bugRatio <= 15 && ( {bugRatio <= 15 && (
<div className="flex items-center gap-2 text-green-600 dark:text-green-400"> <div className="flex items-center gap-2" style={{ color: 'var(--green)' }}>
<span></span> <span></span>
<span>Excellent ratio de bugs ({bugRatio}%) - Bonne qualité du code</span> <span>Excellent ratio de bugs ({bugRatio}%) - Bonne qualité du code</span>
</div> </div>
)} )}
{issueTypes.stories > issueTypes.bugs * 3 && ( {issueTypes.stories > issueTypes.bugs * 3 && (
<div className="flex items-center gap-2 text-blue-600 dark:text-blue-400"> <div className="flex items-center gap-2" style={{ color: 'var(--blue)' }}>
<span>🚀</span> <span>🚀</span>
<span>Focus positif sur les fonctionnalités - Bon équilibre produit</span> <span>Focus positif sur les fonctionnalités - Bon équilibre produit</span>
</div> </div>

View File

@@ -138,16 +138,16 @@ export function ThroughputChart({ sprintHistory, className }: ThroughputChartPro
{/* Légende visuelle */} {/* Légende visuelle */}
<div className="mb-4 flex justify-center gap-6 text-sm"> <div className="mb-4 flex justify-center gap-6 text-sm">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div className="w-4 h-3 bg-blue-600 dark:bg-blue-500 rounded-sm"></div> <div className="w-4 h-3 rounded-sm" style={{ backgroundColor: 'var(--blue)' }}></div>
<span className="text-blue-600 dark:text-blue-500">Points complétés</span> <span style={{ color: 'var(--blue)' }}>Points complétés</span>
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div className="w-4 h-0.5 bg-green-600 dark:bg-green-500"></div> <div className="w-4 h-0.5" style={{ backgroundColor: 'var(--green)' }}></div>
<span className="text-green-600 dark:text-green-500">Throughput</span> <span style={{ color: 'var(--green)' }}>Throughput</span>
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div className="w-4 h-0.5 bg-orange-600 dark:bg-orange-500 border-dashed border-t-2 border-orange-600 dark:border-orange-500"></div> <div className="w-4 h-0.5 border-dashed border-t-2" style={{ backgroundColor: 'var(--accent)', borderColor: 'var(--accent)' }}></div>
<span className="text-orange-600 dark:text-orange-500">Tendance</span> <span style={{ color: 'var(--accent)' }}>Tendance</span>
</div> </div>
</div> </div>

View File

@@ -271,11 +271,14 @@ export default function BackupSettingsPageClient({ initialData }: BackupSettings
if (!message) return null; if (!message) return null;
return ( return (
<div className={`text-xs mt-2 px-2 py-1 rounded transition-all inline-block ${ <div
message.type === 'success' className="text-xs mt-2 px-2 py-1 rounded transition-all inline-block border"
? 'text-green-700 dark:text-green-300 bg-green-50 dark:bg-green-950/20 border border-green-200 dark:border-green-800/20' style={{
: 'text-red-700 dark:text-red-300 bg-red-50 dark:bg-red-950/20 border border-red-200 dark:border-red-800/20' color: message.type === 'success' ? 'var(--success)' : 'var(--destructive)',
}`}> backgroundColor: message.type === 'success' ? 'color-mix(in srgb, var(--success) 10%, transparent)' : 'color-mix(in srgb, var(--destructive) 10%, transparent)',
borderColor: message.type === 'success' ? 'color-mix(in srgb, var(--success) 20%, var(--border))' : 'color-mix(in srgb, var(--destructive) 20%, var(--border))'
}}
>
{message.text} {message.text}
</div> </div>
); );
@@ -561,11 +564,13 @@ export default function BackupSettingsPageClient({ initialData }: BackupSettings
<span className="text-xs text-[var(--muted-foreground)]"> <span className="text-xs text-[var(--muted-foreground)]">
{formatFileSize(backup.size)} {formatFileSize(backup.size)}
</span> </span>
<span className={`text-xs px-1.5 py-0.5 rounded ${ <span
backup.type === 'manual' className="text-xs px-1.5 py-0.5 rounded"
? 'bg-blue-100 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300' style={{
: 'bg-gray-100 dark:bg-gray-800 text-gray-700 dark:text-gray-300' color: backup.type === 'manual' ? 'var(--blue)' : 'var(--muted-foreground)',
}`}> backgroundColor: backup.type === 'manual' ? 'color-mix(in srgb, var(--blue) 10%, transparent)' : 'color-mix(in srgb, var(--muted) 10%, transparent)'
}}
>
{backup.type === 'manual' ? 'Manuel' : 'Auto'} {backup.type === 'manual' ? 'Manuel' : 'Auto'}
</span> </span>
</div> </div>

View File

@@ -341,8 +341,8 @@ export function JiraConfigForm() {
{validationResult && ( {validationResult && (
<div className={`mt-2 p-2 rounded text-sm ${ <div className={`mt-2 p-2 rounded text-sm ${
validationResult.type === 'success' validationResult.type === 'success'
? 'bg-green-50 border border-green-200 text-green-800 dark:bg-green-900/20 dark:border-green-800 dark:text-green-200' ? 'border border-[var(--success)]/20'
: 'bg-red-50 border border-red-200 text-red-800 dark:bg-red-900/20 dark:border-red-800 dark:text-red-200' : 'border border-[var(--destructive)]/20'
}`}> }`}>
{validationResult.text} {validationResult.text}
</div> </div>
@@ -433,11 +433,14 @@ export function JiraConfigForm() {
)} )}
{message && ( {message && (
<div className={`p-4 rounded border ${ <div
message.type === 'success' className="p-4 rounded border"
? 'bg-green-50 border-green-200 text-green-800 dark:bg-green-900/20 dark:border-green-800 dark:text-green-200' style={{
: 'bg-red-50 border-red-200 text-red-800 dark:bg-red-900/20 dark:border-red-800 dark:text-red-200' color: message.type === 'success' ? 'var(--success)' : 'var(--destructive)',
}`}> backgroundColor: message.type === 'success' ? 'color-mix(in srgb, var(--success) 10%, transparent)' : 'color-mix(in srgb, var(--destructive) 10%, transparent)',
borderColor: message.type === 'success' ? 'color-mix(in srgb, var(--success) 20%, var(--border))' : 'color-mix(in srgb, var(--destructive) 20%, var(--border))'
}}
>
{message.text} {message.text}
</div> </div>
)} )}

View File

@@ -339,16 +339,16 @@ export function TfsConfigForm() {
{/* Actions de gestion des données TFS */} {/* Actions de gestion des données TFS */}
{isTfsConfigured && ( {isTfsConfigured && (
<div className="p-4 bg-[var(--card)] rounded border border-orange-200 dark:border-orange-800"> <div className="p-4 bg-[var(--card)] rounded border" style={{ borderColor: 'color-mix(in srgb, var(--accent) 30%, var(--border))', backgroundColor: 'color-mix(in srgb, var(--accent) 5%, var(--card))', color: 'var(--accent)' }}>
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<h3 className="font-medium text-orange-800 dark:text-orange-200"> <h3 className="font-medium" style={{ color: 'var(--accent)' }}>
Gestion des données Gestion des données
</h3> </h3>
<p className="text-sm text-orange-600 dark:text-orange-300"> <p className="text-sm" style={{ color: 'var(--accent)' }}>
Supprimez toutes les tâches TFS synchronisées de la base locale Supprimez toutes les tâches TFS synchronisées de la base locale
</p> </p>
<p className="text-xs text-orange-500 dark:text-orange-400 mt-1"> <p className="text-xs mt-1" style={{ color: 'var(--accent)' }}>
<strong>Attention:</strong> Cette action est irréversible et <strong>Attention:</strong> Cette action est irréversible et
supprimera définitivement toutes les tâches importées depuis supprimera définitivement toutes les tâches importées depuis
Azure DevOps. Azure DevOps.
@@ -624,11 +624,12 @@ export function TfsConfigForm() {
{message && ( {message && (
<div <div
className={`p-4 rounded border ${ className="p-4 rounded border"
message.type === 'success' style={{
? 'bg-green-50 border-green-200 text-green-800 dark:bg-green-900/20 dark:border-green-800 dark:text-green-200' color: message.type === 'success' ? 'var(--success)' : 'var(--destructive)',
: 'bg-red-50 border-red-200 text-red-800 dark:bg-red-900/20 dark:border-red-800 dark:text-red-200' backgroundColor: message.type === 'success' ? 'color-mix(in srgb, var(--success) 10%, transparent)' : 'color-mix(in srgb, var(--destructive) 10%, transparent)',
}`} borderColor: message.type === 'success' ? 'color-mix(in srgb, var(--success) 20%, var(--border))' : 'color-mix(in srgb, var(--destructive) 20%, var(--border))'
}}
> >
{message.text} {message.text}
</div> </div>

View File

@@ -43,11 +43,9 @@ export function QuickActions({
Créer une sauvegarde des données Créer une sauvegarde des données
</p> </p>
{messages.backup && ( {messages.backup && (
<p className={`text-xs mt-1 ${ <p className="text-xs mt-1" style={{
messages.backup.type === 'success' color: messages.backup.type === 'success' ? 'var(--success)' : 'var(--destructive)'
? 'text-green-600 dark:text-green-400' }}>
: 'text-red-600 dark:text-red-400'
}`}>
{messages.backup.text} {messages.backup.text}
</p> </p>
)} )}
@@ -72,11 +70,9 @@ export function QuickActions({
Tester la connexion Jira Tester la connexion Jira
</p> </p>
{messages.jira && ( {messages.jira && (
<p className={`text-xs mt-1 ${ <p className="text-xs mt-1" style={{
messages.jira.type === 'success' color: messages.jira.type === 'success' ? 'var(--success)' : 'var(--destructive)'
? 'text-green-600 dark:text-green-400' }}>
: 'text-red-600 dark:text-red-400'
}`}>
{messages.jira.text} {messages.jira.text}
</p> </p>
)} )}

View File

@@ -8,11 +8,10 @@ import {
updateColumnVisibility as updateColumnVisibilityAction, updateColumnVisibility as updateColumnVisibilityAction,
toggleObjectivesVisibility as toggleObjectivesVisibilityAction, toggleObjectivesVisibility as toggleObjectivesVisibilityAction,
toggleObjectivesCollapse as toggleObjectivesCollapseAction, toggleObjectivesCollapse as toggleObjectivesCollapseAction,
toggleTheme as toggleThemeAction,
setTheme as setThemeAction,
toggleFontSize as toggleFontSizeAction, toggleFontSize as toggleFontSizeAction,
toggleColumnVisibility as toggleColumnVisibilityAction toggleColumnVisibility as toggleColumnVisibilityAction
} from '@/actions/preferences'; } from '@/actions/preferences';
import { useTheme } from './ThemeContext';
interface UserPreferencesContextType { interface UserPreferencesContextType {
preferences: UserPreferences; preferences: UserPreferences;
@@ -77,14 +76,17 @@ const defaultPreferences: UserPreferences = {
export function UserPreferencesProvider({ children, initialPreferences }: UserPreferencesProviderProps) { export function UserPreferencesProvider({ children, initialPreferences }: UserPreferencesProviderProps) {
const [preferences, setPreferences] = useState<UserPreferences>(initialPreferences || defaultPreferences); const [preferences, setPreferences] = useState<UserPreferences>(initialPreferences || defaultPreferences);
const [isPending, startTransition] = useTransition(); const [isPending, startTransition] = useTransition();
const { theme, toggleTheme: themeToggleTheme, setTheme: themeSetTheme } = useTheme();
// Synchroniser le thème avec le ThemeProvider global (si disponible) // Synchroniser les préférences avec le thème actuel du ThemeContext
useEffect(() => { useEffect(() => {
if (typeof window !== 'undefined') { if (preferences.viewPreferences.theme !== theme) {
// Appliquer le thème au document setPreferences(prev => ({
document.documentElement.className = preferences.viewPreferences.theme; ...prev,
viewPreferences: { ...prev.viewPreferences, theme }
}));
} }
}, [preferences.viewPreferences.theme]); }, [theme, preferences.viewPreferences.theme]);
// === KANBAN FILTERS === // === KANBAN FILTERS ===
@@ -149,16 +151,12 @@ export function UserPreferencesProvider({ children, initialPreferences }: UserPr
}, []); }, []);
const toggleTheme = useCallback(() => { const toggleTheme = useCallback(() => {
startTransition(async () => { themeToggleTheme();
await toggleThemeAction(); }, [themeToggleTheme]);
});
}, []);
const setTheme = useCallback((theme: 'light' | 'dark') => { const setTheme = useCallback((theme: 'light' | 'dark') => {
startTransition(async () => { themeSetTheme(theme);
await setThemeAction(theme); }, [themeSetTheme]);
});
}, []);
const toggleFontSize = useCallback(() => { const toggleFontSize = useCallback(() => {
startTransition(async () => { startTransition(async () => {