feat: update TODO and enhance design token integration

- Marked hydration issues and design system tasks as complete in TODO.md, reflecting progress on theme optimization.
- Added documentation for CSS variables in globals.css to guide future color modifications using design tokens.
- Refactored QuickActions component to utilize StatusMessage for better message display and applied design tokens for button styles, improving UI consistency.
This commit is contained in:
Julien Froidefond
2025-09-28 10:21:39 +02:00
parent b5d6967fcd
commit aa348a0f82
8 changed files with 696 additions and 14 deletions

View File

@@ -16,8 +16,9 @@
- [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
- [x] **Corriger les problèmes d'hydration** mismatch et flashs de thème <!-- Script inline + ThemeContext optimisé -->
- [x] **Créer un système de design cohérent** avec tokens de couleur <!-- Design tokens + composants utilitaires + documentation -->
- [ ] **MIGRATION MASSIVE : Refactorer tous les composants** pour utiliser les design tokens <!-- Gros travail de migration composant par composant -->
### **Phase 2: Système Couleurs Personnalisées**
- [ ] **Étendre le modèle UserPreferences** pour supporter des couleurs personnalisées

View File

@@ -1,5 +1,14 @@
@import "tailwindcss";
/*
* CSS Variables - Design Tokens
*
* Ces variables sont synchronisées avec src/lib/design-tokens.ts
* Pour modifier les couleurs, utilisez les design tokens plutôt que ce fichier directement.
*
* Documentation: src/lib/DESIGN_SYSTEM.md
*/
:root {
/* Valeurs par défaut (Light theme) */
--background: #f1f5f9; /* slate-100 */

View File

@@ -1,6 +1,8 @@
'use client';
import { Card, CardContent } from '@/components/ui/Card';
import { StatusMessage } from '@/components/ui/StatusMessage';
import { useDesignTokens } from '@/hooks/useDesignTokens';
interface Message {
type: 'success' | 'error';
@@ -27,6 +29,7 @@ export function QuickActions({
jiraEnabled,
messages
}: QuickActionsProps) {
const { styles } = useDesignTokens();
return (
<div className="mt-8">
<h2 className="text-xl font-semibold text-[var(--foreground)] mb-4">
@@ -43,17 +46,18 @@ export function QuickActions({
Créer une sauvegarde des données
</p>
{messages.backup && (
<p className="text-xs mt-1" style={{
color: messages.backup.type === 'success' ? 'var(--success)' : 'var(--destructive)'
}}>
{messages.backup.text}
</p>
<div className="mt-2">
<StatusMessage type={messages.backup.type}>
{messages.backup.text}
</StatusMessage>
</div>
)}
</div>
<button
onClick={onCreateBackup}
disabled={isBackupLoading}
className="px-3 py-1.5 bg-[var(--primary)] text-[var(--primary-foreground)] rounded text-sm disabled:opacity-50 disabled:cursor-not-allowed"
className="px-3 py-1.5 rounded text-sm disabled:opacity-50 disabled:cursor-not-allowed"
style={styles.primaryButton}
>
{isBackupLoading ? 'En cours...' : 'Sauvegarder'}
</button>
@@ -70,17 +74,18 @@ export function QuickActions({
Tester la connexion Jira
</p>
{messages.jira && (
<p className="text-xs mt-1" style={{
color: messages.jira.type === 'success' ? 'var(--success)' : 'var(--destructive)'
}}>
{messages.jira.text}
</p>
<div className="mt-2">
<StatusMessage type={messages.jira.type}>
{messages.jira.text}
</StatusMessage>
</div>
)}
</div>
<button
onClick={onTestJira}
disabled={!jiraEnabled || isJiraTestLoading}
className="px-3 py-1.5 bg-[var(--card)] text-[var(--foreground)] border border-[var(--border)] rounded text-sm disabled:opacity-50 disabled:cursor-not-allowed"
className="px-3 py-1.5 rounded text-sm disabled:opacity-50 disabled:cursor-not-allowed"
style={styles.input}
>
{isJiraTestLoading ? 'Test...' : 'Tester'}
</button>

View File

@@ -0,0 +1,57 @@
/**
* Composant de carte colorée utilisant les design tokens
*/
import { ReactNode } from 'react';
import { useDesignTokens } from '@/hooks/useDesignTokens';
interface ColoredCardProps {
color: 'primary' | 'purple' | 'yellow' | 'green' | 'blue' | 'gray';
children: ReactNode;
className?: string;
hover?: boolean;
}
export function ColoredCard({ color, children, className = '', hover = true }: ColoredCardProps) {
const { styles } = useDesignTokens();
const getStyle = () => {
switch (color) {
case 'primary':
return styles.primaryCard;
case 'purple':
return styles.purpleCard;
case 'yellow':
return styles.yellowCard;
case 'green':
return {
color: 'var(--green)',
backgroundColor: 'color-mix(in srgb, var(--green) 10%, transparent)',
borderColor: 'color-mix(in srgb, var(--green) 20%, var(--border))',
};
case 'blue':
return {
color: 'var(--blue)',
backgroundColor: 'color-mix(in srgb, var(--blue) 10%, transparent)',
borderColor: 'color-mix(in srgb, var(--blue) 20%, var(--border))',
};
case 'gray':
return {
color: 'var(--muted-foreground)',
backgroundColor: 'color-mix(in srgb, var(--muted) 10%, transparent)',
borderColor: 'color-mix(in srgb, var(--muted) 20%, var(--border))',
};
default:
return styles.primaryCard;
}
};
return (
<div
className={`p-4 rounded-lg border transition-all ${hover ? 'hover:shadow-sm hover:scale-[1.01]' : ''} ${className}`}
style={getStyle()}
>
{children}
</div>
);
}

View File

@@ -0,0 +1,58 @@
/**
* Composant pour afficher des messages d'état avec les design tokens
*/
import { ReactNode } from 'react';
import { useDesignTokens } from '@/hooks/useDesignTokens';
interface StatusMessageProps {
type: 'success' | 'error' | 'warning' | 'info';
children: ReactNode;
className?: string;
}
export function StatusMessage({ type, children, className = '' }: StatusMessageProps) {
const { styles } = useDesignTokens();
const getStyle = () => {
switch (type) {
case 'success':
return styles.successMessage;
case 'error':
return styles.errorMessage;
case 'warning':
return styles.warningMessage;
case 'info':
return styles.primaryCard;
default:
return styles.primaryCard;
}
};
const getIcon = () => {
switch (type) {
case 'success':
return '✅';
case 'error':
return '❌';
case 'warning':
return '⚠️';
case 'info':
return '';
default:
return '';
}
};
return (
<div
className={`p-3 rounded border ${className}`}
style={getStyle()}
>
<div className="flex items-center gap-2">
<span>{getIcon()}</span>
<span>{children}</span>
</div>
</div>
);
}

View File

@@ -0,0 +1,112 @@
/**
* Hook pour utiliser les design tokens dans les composants
*/
import { useTheme } from '@/contexts/ThemeContext';
import { DesignTokenUtils, COLOR_PATTERNS, OPACITY } from '@/lib/design-tokens';
export function useDesignTokens() {
const { theme } = useTheme();
return {
theme,
// Utilitaires de couleur
withOpacity: DesignTokenUtils.withOpacity,
borderWithOpacity: DesignTokenUtils.borderWithOpacity,
backgroundWithOpacity: DesignTokenUtils.backgroundWithOpacity,
// Patterns prédéfinis
patterns: COLOR_PATTERNS,
// Opacités courantes
opacity: OPACITY,
// Couleurs CSS Variables (pour usage direct)
colors: {
background: 'var(--background)',
foreground: 'var(--foreground)',
card: 'var(--card)',
cardHover: 'var(--card-hover)',
cardColumn: 'var(--card-column)',
border: 'var(--border)',
input: 'var(--input)',
primary: 'var(--primary)',
primaryForeground: 'var(--primary-foreground)',
muted: 'var(--muted)',
mutedForeground: 'var(--muted-foreground)',
destructive: 'var(--destructive)',
success: 'var(--success)',
accent: 'var(--accent)',
purple: 'var(--purple)',
yellow: 'var(--yellow)',
green: 'var(--green)',
blue: 'var(--blue)',
gray: 'var(--gray)',
grayLight: 'var(--gray-light)',
},
// Styles prédéfinis pour les composants courants
styles: {
// Messages d'état
successMessage: {
color: COLOR_PATTERNS.success.text,
backgroundColor: COLOR_PATTERNS.success.background,
borderColor: COLOR_PATTERNS.success.border,
},
errorMessage: {
color: COLOR_PATTERNS.error.text,
backgroundColor: COLOR_PATTERNS.error.background,
borderColor: COLOR_PATTERNS.error.border,
},
warningMessage: {
color: COLOR_PATTERNS.warning.text,
backgroundColor: COLOR_PATTERNS.warning.background,
borderColor: COLOR_PATTERNS.warning.border,
},
// Cards colorées
primaryCard: {
color: COLOR_PATTERNS.primary.text,
backgroundColor: COLOR_PATTERNS.primary.background,
borderColor: COLOR_PATTERNS.primary.border,
},
purpleCard: {
color: COLOR_PATTERNS.purple.text,
backgroundColor: COLOR_PATTERNS.purple.background,
borderColor: COLOR_PATTERNS.purple.border,
},
yellowCard: {
color: COLOR_PATTERNS.yellow.text,
backgroundColor: COLOR_PATTERNS.yellow.background,
borderColor: COLOR_PATTERNS.yellow.border,
},
// Boutons
primaryButton: {
backgroundColor: 'var(--primary)',
color: 'var(--primary-foreground)',
borderColor: 'var(--primary)',
},
destructiveButton: {
backgroundColor: 'var(--destructive)',
color: 'white',
borderColor: 'var(--destructive)',
},
// Inputs
input: {
backgroundColor: 'var(--input)',
borderColor: 'var(--border)',
color: 'var(--foreground)',
},
// Scrollbars
scrollbar: {
track: 'var(--card)',
thumb: 'var(--muted)',
thumbHover: 'var(--primary)',
},
},
};
}

199
src/lib/DESIGN_SYSTEM.md Normal file
View File

@@ -0,0 +1,199 @@
# Design System - TowerControl
## Vue d'ensemble
Le design system de TowerControl utilise des **Design Tokens** pour assurer la cohérence visuelle et faciliter la maintenance des couleurs et styles.
## Architecture
### 1. Design Tokens (`src/lib/design-tokens.ts`)
Source unique de vérité pour toutes les couleurs et styles :
```typescript
import { DesignTokenUtils, COLOR_PATTERNS } from '@/lib/design-tokens';
// Utilisation des utilitaires
const backgroundColor = DesignTokenUtils.withOpacity('var(--primary)', 0.1);
const borderColor = DesignTokenUtils.borderWithOpacity('var(--success)', 0.2, 'var(--border)');
```
### 2. Hook useDesignTokens (`src/hooks/useDesignTokens.ts`)
Hook React pour utiliser facilement les tokens dans les composants :
```tsx
import { useDesignTokens } from '@/hooks/useDesignTokens';
function MyComponent() {
const { colors, styles, patterns } = useDesignTokens();
return (
<div style={{ color: colors.primary }}>
<div style={styles.successMessage}>Succès !</div>
</div>
);
}
```
### 3. Composants utilitaires
#### StatusMessage
```tsx
import { StatusMessage } from '@/components/ui/StatusMessage';
<StatusMessage type="success">Opération réussie</StatusMessage>
<StatusMessage type="error">Une erreur est survenue</StatusMessage>
<StatusMessage type="warning">Attention requise</StatusMessage>
<StatusMessage type="info">Information</StatusMessage>
```
#### ColoredCard
```tsx
import { ColoredCard } from '@/components/ui/ColoredCard';
<ColoredCard color="primary">Contenu principal</ColoredCard>
<ColoredCard color="purple">Contenu violet</ColoredCard>
<ColoredCard color="yellow">Contenu jaune</ColoredCard>
```
## Couleurs disponibles
### Couleurs principales
- `--background` : Arrière-plan principal
- `--foreground` : Texte principal
- `--card` : Arrière-plan des cartes
- `--card-hover` : État hover des cartes
- `--border` : Couleur des bordures
- `--input` : Arrière-plan des inputs
### Couleurs sémantiques
- `--primary` : Couleur principale (cyan)
- `--success` : Succès (vert)
- `--destructive` : Erreur/danger (rouge)
- `--accent` : Accent (orange/amber)
- `--muted` : Texte atténué
- `--muted-foreground` : Texte secondaire
### Couleurs étendues
- `--purple` : Violet
- `--yellow` : Jaune
- `--green` : Vert
- `--blue` : Bleu
- `--gray` : Gris
- `--gray-light` : Gris clair
## Patterns de couleurs
### Messages d'état
```typescript
const { patterns } = useDesignTokens();
// Utilisation directe
<div style={patterns.success}>Message de succès</div>
<div style={patterns.error}>Message d'erreur</div>
<div style={patterns.warning}>Message d'avertissement</div>
```
### Opacités courantes
```typescript
const { opacity } = useDesignTokens();
// Opacités prédéfinies
opacity.subtle // 5% - très subtil
opacity.light // 10% - léger
opacity.medium // 20% - moyen
opacity.strong // 30% - fort
opacity.solid // 80% - presque opaque
```
## Bonnes pratiques
### ✅ Recommandé
```tsx
// Utiliser les design tokens
const { colors, styles } = useDesignTokens();
<div style={{ color: colors.primary }}>
<div style={styles.successMessage}>
// Utiliser les composants utilitaires
<StatusMessage type="success">Succès</StatusMessage>
<ColoredCard color="primary">Contenu</ColoredCard>
```
### ❌ À éviter
```tsx
// Couleurs hardcodées
<div style={{ color: '#0891b2' }}>
<div className="text-blue-600">
// Classes Tailwind dark:
<div className="bg-white dark:bg-gray-800">
```
## Migration des composants existants
### Avant (avec classes Tailwind)
```tsx
<div className="bg-green-50 border border-green-200 text-green-800 dark:bg-green-900/20 dark:border-green-800 dark:text-green-200">
Succès
</div>
```
### Après (avec design tokens)
```tsx
<StatusMessage type="success">
Succès
</StatusMessage>
```
## Extensibilité
Le système est conçu pour supporter facilement :
- **Nouveaux thèmes** : Ajouter dans `themeTokens`
- **Nouveaux patterns** : Étendre `COLOR_PATTERNS`
- **Nouvelles opacités** : Ajouter dans `OPACITY`
- **Composants personnalisés** : Utiliser `useDesignTokens`
## Exemples d'usage
### Composant personnalisé
```tsx
import { useDesignTokens } from '@/hooks/useDesignTokens';
function CustomComponent() {
const { colors, withOpacity } = useDesignTokens();
return (
<div
style={{
backgroundColor: withOpacity(colors.primary, 0.1),
borderColor: colors.primary,
color: colors.primary
}}
>
Contenu personnalisé
</div>
);
}
```
### Style conditionnel
```tsx
function ConditionalComponent({ isActive }: { isActive: boolean }) {
const { colors } = useDesignTokens();
return (
<div
style={{
color: isActive ? colors.success : colors.mutedForeground
}}
>
{isActive ? 'Actif' : 'Inactif'}
</div>
);
}
```
Ce système garantit la cohérence visuelle et facilite la maintenance des styles dans toute l'application.

241
src/lib/design-tokens.ts Normal file
View File

@@ -0,0 +1,241 @@
/**
* Design Tokens - Système de couleurs cohérent pour TowerControl
*
* Ce fichier définit tous les tokens de design utilisés dans l'application.
* Il sert de source unique de vérité pour les couleurs et facilite la maintenance.
*/
export type ColorToken = string;
export interface ColorPalette {
// Couleurs principales
background: ColorToken;
foreground: ColorToken;
// Surfaces
card: ColorToken;
cardHover: ColorToken;
cardColumn: ColorToken;
// Bordures et inputs
border: ColorToken;
input: ColorToken;
// Couleurs sémantiques
primary: ColorToken;
primaryForeground: ColorToken;
muted: ColorToken;
mutedForeground: ColorToken;
// Couleurs d'état
destructive: ColorToken;
success: ColorToken;
accent: ColorToken;
// Couleurs étendues
purple: ColorToken;
yellow: ColorToken;
green: ColorToken;
blue: ColorToken;
gray: ColorToken;
grayLight: ColorToken;
}
export interface ThemeTokens {
light: ColorPalette;
dark: ColorPalette;
}
/**
* Tokens de couleurs pour le thème clair
*/
export const lightTheme: ColorPalette = {
// Couleurs principales
background: '#f1f5f9', // slate-100
foreground: '#0f172a', // slate-900
// Surfaces
card: '#ffffff', // white
cardHover: '#f8fafc', // slate-50
cardColumn: '#f8fafc', // slate-50
// Bordures et inputs
border: '#cbd5e1', // slate-300
input: '#ffffff', // white
// Couleurs sémantiques
primary: '#0891b2', // cyan-600
primaryForeground: '#ffffff', // white
muted: '#94a3b8', // slate-400
mutedForeground: '#64748b', // slate-500
// Couleurs d'état
destructive: '#dc2626', // red-600
success: '#059669', // emerald-600
accent: '#d97706', // amber-600
// Couleurs étendues
purple: '#8b5cf6', // purple-500
yellow: '#eab308', // yellow-500
green: '#059669', // emerald-600
blue: '#2563eb', // blue-600
gray: '#6b7280', // gray-500
grayLight: '#e5e7eb', // gray-200
};
/**
* Tokens de couleurs pour le thème sombre
*/
export const darkTheme: ColorPalette = {
// Couleurs principales
background: '#1e293b', // slate-800
foreground: '#f1f5f9', // slate-100
// Surfaces
card: '#334155', // slate-700
cardHover: '#475569', // slate-600
cardColumn: '#0f172a', // slate-900
// Bordures et inputs
border: '#64748b', // slate-500
input: '#334155', // slate-700
// Couleurs sémantiques
primary: '#06b6d4', // cyan-500
primaryForeground: '#f1f5f9', // slate-100
muted: '#64748b', // slate-500
mutedForeground: '#94a3b8', // slate-400
// Couleurs d'état
destructive: '#ef4444', // red-500
success: '#10b981', // emerald-500
accent: '#f59e0b', // amber-500
// Couleurs étendues
purple: '#8b5cf6', // purple-500
yellow: '#eab308', // yellow-500
green: '#10b981', // emerald-500
blue: '#3b82f6', // blue-500
gray: '#9ca3af', // gray-400
grayLight: '#374151', // gray-700
};
/**
* Collection complète des tokens de thème
*/
export const themeTokens: ThemeTokens = {
light: lightTheme,
dark: darkTheme,
};
/**
* Utilitaires pour les tokens de couleur
*/
export class DesignTokenUtils {
/**
* Génère une couleur avec transparence
*/
static withOpacity(color: ColorToken, opacity: number): string {
return `color-mix(in srgb, ${color} ${opacity * 100}%, transparent)`;
}
/**
* Génère une couleur de bordure avec transparence
*/
static borderWithOpacity(color: ColorToken, opacity: number, baseBorder: ColorToken): string {
return `color-mix(in srgb, ${color} ${opacity * 100}%, ${baseBorder})`;
}
/**
* Génère un background avec transparence
*/
static backgroundWithOpacity(color: ColorToken, opacity: number): string {
return `color-mix(in srgb, ${color} ${opacity * 100}%, transparent)`;
}
/**
* Obtient les tokens pour un thème donné
*/
static getTokens(theme: 'light' | 'dark'): ColorPalette {
return themeTokens[theme];
}
/**
* Génère les CSS Variables pour un thème
*/
static generateCSSVariables(theme: 'light' | 'dark'): Record<string, string> {
const tokens = this.getTokens(theme);
return {
'--background': tokens.background,
'--foreground': tokens.foreground,
'--card': tokens.card,
'--card-hover': tokens.cardHover,
'--card-column': tokens.cardColumn,
'--border': tokens.border,
'--input': tokens.input,
'--primary': tokens.primary,
'--primary-foreground': tokens.primaryForeground,
'--muted': tokens.muted,
'--muted-foreground': tokens.mutedForeground,
'--destructive': tokens.destructive,
'--success': tokens.success,
'--accent': tokens.accent,
'--purple': tokens.purple,
'--yellow': tokens.yellow,
'--green': tokens.green,
'--blue': tokens.blue,
'--gray': tokens.gray,
'--gray-light': tokens.grayLight,
};
}
}
/**
* Constantes pour les opacités courantes
*/
export const OPACITY = {
subtle: 0.05, // 5% - très subtil
light: 0.1, // 10% - léger
medium: 0.2, // 20% - moyen
strong: 0.3, // 30% - fort
solid: 0.8, // 80% - presque opaque
} as const;
/**
* Patterns de couleurs prédéfinis pour les composants
*/
export const COLOR_PATTERNS = {
// Messages d'état
success: {
text: 'var(--success)',
background: DesignTokenUtils.withOpacity('var(--success)', OPACITY.light),
border: DesignTokenUtils.borderWithOpacity('var(--success)', OPACITY.medium, 'var(--border)'),
},
error: {
text: 'var(--destructive)',
background: DesignTokenUtils.withOpacity('var(--destructive)', OPACITY.light),
border: DesignTokenUtils.borderWithOpacity('var(--destructive)', OPACITY.medium, 'var(--border)'),
},
warning: {
text: 'var(--accent)',
background: DesignTokenUtils.withOpacity('var(--accent)', OPACITY.light),
border: DesignTokenUtils.borderWithOpacity('var(--accent)', OPACITY.medium, 'var(--border)'),
},
// Cards colorées
primary: {
text: 'var(--primary)',
background: DesignTokenUtils.withOpacity('var(--primary)', OPACITY.light),
border: DesignTokenUtils.borderWithOpacity('var(--primary)', OPACITY.medium, 'var(--border)'),
},
purple: {
text: 'var(--purple)',
background: DesignTokenUtils.withOpacity('var(--purple)', OPACITY.light),
border: DesignTokenUtils.borderWithOpacity('var(--purple)', OPACITY.medium, 'var(--border)'),
},
yellow: {
text: 'var(--yellow)',
background: DesignTokenUtils.withOpacity('var(--yellow)', OPACITY.light),
border: DesignTokenUtils.borderWithOpacity('var(--yellow)', OPACITY.medium, 'var(--border)'),
},
} as const;