diff --git a/TODO.md b/TODO.md index f1eec0e..cd6438d 100644 --- a/TODO.md +++ b/TODO.md @@ -16,8 +16,9 @@ - [x] **Supprimer la double application** du thème (layout.tsx + ThemeContext + UserPreferencesContext) - [x] **Refactorer les CSS variables** : `:root` pour défaut, `.dark/.light` pour override - [x] **Nettoyer les composants** : supprimer classes `dark:` hardcodées, utiliser uniquement CSS variables -- [ ] **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 +- [x] **Créer un système de design cohérent** avec tokens de couleur +- [ ] **MIGRATION MASSIVE : Refactorer tous les composants** pour utiliser les design tokens ### **Phase 2: Système Couleurs Personnalisées** - [ ] **Étendre le modèle UserPreferences** pour supporter des couleurs personnalisées diff --git a/src/app/globals.css b/src/app/globals.css index 35de1c9..1b64ec9 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -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 */ diff --git a/src/components/settings/index/QuickActions.tsx b/src/components/settings/index/QuickActions.tsx index 8d5512d..bf2a1e0 100644 --- a/src/components/settings/index/QuickActions.tsx +++ b/src/components/settings/index/QuickActions.tsx @@ -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 (

@@ -43,17 +46,18 @@ export function QuickActions({ Créer une sauvegarde des données

{messages.backup && ( -

- {messages.backup.text} -

+
+ + {messages.backup.text} + +
)}

@@ -70,17 +74,18 @@ export function QuickActions({ Tester la connexion Jira

{messages.jira && ( -

- {messages.jira.text} -

+
+ + {messages.jira.text} + +
)} diff --git a/src/components/ui/ColoredCard.tsx b/src/components/ui/ColoredCard.tsx new file mode 100644 index 0000000..77d54c2 --- /dev/null +++ b/src/components/ui/ColoredCard.tsx @@ -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 ( +
+ {children} +
+ ); +} diff --git a/src/components/ui/StatusMessage.tsx b/src/components/ui/StatusMessage.tsx new file mode 100644 index 0000000..d6ca081 --- /dev/null +++ b/src/components/ui/StatusMessage.tsx @@ -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 ( +
+
+ {getIcon()} + {children} +
+
+ ); +} diff --git a/src/hooks/useDesignTokens.ts b/src/hooks/useDesignTokens.ts new file mode 100644 index 0000000..d5a4065 --- /dev/null +++ b/src/hooks/useDesignTokens.ts @@ -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)', + }, + }, + }; +} diff --git a/src/lib/DESIGN_SYSTEM.md b/src/lib/DESIGN_SYSTEM.md new file mode 100644 index 0000000..fd1b02a --- /dev/null +++ b/src/lib/DESIGN_SYSTEM.md @@ -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 ( +
+
Succès !
+
+ ); +} +``` + +### 3. Composants utilitaires + +#### StatusMessage +```tsx +import { StatusMessage } from '@/components/ui/StatusMessage'; + +Opération réussie +Une erreur est survenue +Attention requise +Information +``` + +#### ColoredCard +```tsx +import { ColoredCard } from '@/components/ui/ColoredCard'; + +Contenu principal +Contenu violet +Contenu jaune +``` + +## 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 +
Message de succès
+
Message d'erreur
+
Message d'avertissement
+``` + +### 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(); + +
+
+ +// Utiliser les composants utilitaires +Succès +Contenu +``` + +### ❌ À éviter +```tsx +// Couleurs hardcodées +
+
+ +// Classes Tailwind dark: +
+``` + +## Migration des composants existants + +### Avant (avec classes Tailwind) +```tsx +
+ ✅ Succès +
+``` + +### Après (avec design tokens) +```tsx + + Succès + +``` + +## 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 ( +
+ Contenu personnalisé +
+ ); +} +``` + +### Style conditionnel +```tsx +function ConditionalComponent({ isActive }: { isActive: boolean }) { + const { colors } = useDesignTokens(); + + return ( +
+ {isActive ? 'Actif' : 'Inactif'} +
+ ); +} +``` + +Ce système garantit la cohérence visuelle et facilite la maintenance des styles dans toute l'application. diff --git a/src/lib/design-tokens.ts b/src/lib/design-tokens.ts new file mode 100644 index 0000000..f3f81d7 --- /dev/null +++ b/src/lib/design-tokens.ts @@ -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 { + 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;