From 0e2eaf1052ec3a8433ee3b189e2195a407ad847a Mon Sep 17 00:00:00 2001 From: Julien Froidefond Date: Sun, 28 Sep 2025 21:08:48 +0200 Subject: [PATCH] feat: improve theme selector and UI components - Updated `ThemeSelector` to use a new `ThemePreview` component for better theme visualization. - Refactored button implementation in `ThemeSelector` to utilize the new `Button` component, enhancing consistency. - Added a UI showcase section in `GeneralSettingsPageClient` to display available UI components with different themes. - Enhanced `Badge`, `Button`, and `Input` components with new variants and improved styling for better usability and visual appeal. - Updated CSS variables in `globals.css` for improved contrast and accessibility across themes. --- UI_COMPONENTS_GUIDE.md | 106 ++++++ src/app/globals.css | 22 +- src/app/ui-showcase/page.tsx | 5 + src/components/ThemeSelector.tsx | 177 +++------- .../settings/GeneralSettingsPageClient.tsx | 22 ++ .../ui-showcase/UIShowcaseClient.tsx | 327 ++++++++++++++++++ src/components/ui/Alert.tsx | 58 ++++ src/components/ui/Badge.tsx | 43 +-- src/components/ui/Button.tsx | 30 +- src/components/ui/Input.tsx | 46 +-- src/components/ui/StyledCard.tsx | 49 +++ src/components/ui/index.ts | 11 + 12 files changed, 694 insertions(+), 202 deletions(-) create mode 100644 UI_COMPONENTS_GUIDE.md create mode 100644 src/app/ui-showcase/page.tsx create mode 100644 src/components/ui-showcase/UIShowcaseClient.tsx create mode 100644 src/components/ui/Alert.tsx create mode 100644 src/components/ui/StyledCard.tsx create mode 100644 src/components/ui/index.ts diff --git a/UI_COMPONENTS_GUIDE.md b/UI_COMPONENTS_GUIDE.md new file mode 100644 index 0000000..1f5a08b --- /dev/null +++ b/UI_COMPONENTS_GUIDE.md @@ -0,0 +1,106 @@ +# Guide des Composants UI + +## 🎯 Principe + +**Les composants métier ne doivent JAMAIS utiliser directement les variables CSS.** Ils doivent utiliser les composants UI abstraits. + +## ❌ MAUVAIS + +```tsx +// ❌ Composant métier avec variables CSS directes +function TaskCard({ task }) { + return ( +
+ +
+ ); +} +``` + +## ✅ BON + +```tsx +// ✅ Composant métier utilisant les composants UI +import { Card, CardContent, Button } from '@/components/ui'; + +function TaskCard({ task }) { + return ( + + + + + + ); +} +``` + +## 📦 Composants UI Disponibles + +### Button +```tsx + + + + +``` + +### Badge +```tsx +Tag +Succès +Erreur +``` + +### Alert +```tsx + + Succès + Opération réussie + +``` + +### Input +```tsx + + +``` + +### StyledCard +```tsx + + Contenu avec style coloré + +``` + +## 🔄 Migration + +### Étape 1: Identifier les patterns +- Rechercher `var(--` dans les composants métier +- Identifier les patterns répétés (boutons, cartes, badges) + +### Étape 2: Créer des composants UI +- Encapsuler les styles dans des composants UI +- Utiliser des variants pour les variations + +### Étape 3: Remplacer dans les composants métier +- Importer les composants UI +- Remplacer les éléments HTML par les composants UI + +## 🎨 Avantages + +1. **Consistance** - Même apparence partout +2. **Maintenance** - Changements centralisés +3. **Réutilisabilité** - Composants réutilisables +4. **Type Safety** - Props typées +5. **Performance** - Styles optimisés + +## 📝 Règles + +1. **JAMAIS** de variables CSS dans les composants métier +2. **TOUJOURS** utiliser les composants UI +3. **CRÉER** de nouveaux composants UI si nécessaire +4. **DOCUMENTER** les nouveaux composants UI diff --git a/src/app/globals.css b/src/app/globals.css index ab2795f..892c976 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -34,7 +34,7 @@ --border: #64748b; /* slate-500 - encore plus clair */ --input: #334155; /* slate-700 - plus clair */ --primary: #06b6d4; /* cyan-500 */ - --primary-foreground: #f1f5f9; /* slate-100 */ + --primary-foreground: #ffffff; /* white - better contrast with cyan */ --muted: #64748b; /* slate-500 */ --muted-foreground: #94a3b8; /* slate-400 */ --accent: #f59e0b; /* amber-500 */ @@ -58,7 +58,7 @@ --border: #6272a4; /* dracula comment */ --input: #44475a; /* dracula current line */ --primary: #ff79c6; /* dracula pink */ - --primary-foreground: #282a36; /* dracula background */ + --primary-foreground: #ffffff; /* white for contrast */ --muted: #6272a4; /* dracula comment */ --muted-foreground: #50fa7b; /* dracula green */ --accent: #ffb86c; /* dracula orange */ @@ -82,7 +82,7 @@ --border: #49483e; /* monokai line */ --input: #3e3d32; /* monokai selection */ --primary: #f92672; /* monokai pink */ - --primary-foreground: #272822; /* monokai background */ + --primary-foreground: #ffffff; /* white for contrast */ --muted: #75715e; /* monokai comment */ --muted-foreground: #a6e22e; /* monokai green */ --accent: #fd971f; /* monokai orange */ @@ -106,7 +106,7 @@ --border: #4c566a; /* nord3 */ --input: #3b4252; /* nord1 */ --primary: #88c0d0; /* nord7 */ - --primary-foreground: #2e3440; /* nord0 */ + --primary-foreground: #ffffff; /* white for contrast */ --muted: #4c566a; /* nord3 */ --muted-foreground: #81a1c1; /* nord9 */ --accent: #d08770; /* nord12 */ @@ -130,7 +130,7 @@ --border: #665c54; /* gruvbox bg3 */ --input: #3c3836; /* gruvbox bg1 */ --primary: #fe8019; /* gruvbox orange */ - --primary-foreground: #282828; /* gruvbox bg0 */ + --primary-foreground: #ffffff; /* white for contrast */ --muted: #665c54; /* gruvbox bg3 */ --muted-foreground: #a89984; /* gruvbox gray */ --accent: #fabd2f; /* gruvbox yellow */ @@ -154,7 +154,7 @@ --border: #565f89; /* tokyo-night comment */ --input: #24283b; /* tokyo-night bg_highlight */ --primary: #7aa2f7; /* tokyo-night blue */ - --primary-foreground: #1a1b26; /* tokyo-night bg */ + --primary-foreground: #ffffff; /* white for contrast */ --muted: #565f89; /* tokyo-night comment */ --muted-foreground: #9aa5ce; /* tokyo-night fg_dark */ --accent: #ff9e64; /* tokyo-night orange */ @@ -178,7 +178,7 @@ --border: #6c7086; /* catppuccin overlay0 */ --input: #313244; /* catppuccin surface0 */ --primary: #cba6f7; /* catppuccin mauve */ - --primary-foreground: #1e1e2e; /* catppuccin base */ + --primary-foreground: #ffffff; /* white for contrast */ --muted: #6c7086; /* catppuccin overlay0 */ --muted-foreground: #a6adc8; /* catppuccin subtext0 */ --accent: #fab387; /* catppuccin peach */ @@ -202,7 +202,7 @@ --border: #6e6a86; /* rose-pine muted */ --input: #26233a; /* rose-pine surface */ --primary: #c4a7e7; /* rose-pine iris */ - --primary-foreground: #191724; /* rose-pine base */ + --primary-foreground: #ffffff; /* white for contrast */ --muted: #6e6a86; /* rose-pine muted */ --muted-foreground: #908caa; /* rose-pine subtle */ --accent: #f6c177; /* rose-pine gold */ @@ -226,7 +226,7 @@ --border: #5c6370; /* one-dark bg3 */ --input: #3e4451; /* one-dark bg1 */ --primary: #61afef; /* one-dark blue */ - --primary-foreground: #282c34; /* one-dark bg */ + --primary-foreground: #ffffff; /* white for contrast */ --muted: #5c6370; /* one-dark bg3 */ --muted-foreground: #828997; /* one-dark gray */ --accent: #e06c75; /* one-dark red */ @@ -250,7 +250,7 @@ --border: #3c3c3c; /* material outline */ --input: #1e1e1e; /* material surface */ --primary: #bb86fc; /* material primary */ - --primary-foreground: #121212; /* material bg */ + --primary-foreground: #ffffff; /* white for contrast */ --muted: #3c3c3c; /* material outline */ --muted-foreground: #b3b3b3; /* material on-surface-variant */ --accent: #ffab40; /* material secondary */ @@ -274,7 +274,7 @@ --border: #586e75; /* solarized base01 */ --input: #073642; /* solarized base02 */ --primary: #268bd2; /* solarized blue */ - --primary-foreground: #002b36; /* solarized base03 */ + --primary-foreground: #ffffff; /* white for contrast */ --muted: #586e75; /* solarized base01 */ --muted-foreground: #657b83; /* solarized base00 */ --accent: #b58900; /* solarized yellow */ diff --git a/src/app/ui-showcase/page.tsx b/src/app/ui-showcase/page.tsx new file mode 100644 index 0000000..973b8e6 --- /dev/null +++ b/src/app/ui-showcase/page.tsx @@ -0,0 +1,5 @@ +import { UIShowcaseClient } from '@/components/ui-showcase/UIShowcaseClient'; + +export default function UIShowcasePage() { + return ; +} diff --git a/src/components/ThemeSelector.tsx b/src/components/ThemeSelector.tsx index d90de4a..39eeb03 100644 --- a/src/components/ThemeSelector.tsx +++ b/src/components/ThemeSelector.tsx @@ -2,6 +2,8 @@ import { useTheme } from '@/contexts/ThemeContext'; import { Theme } from '@/lib/types'; +import { Button } from '@/components/ui/Button'; + const themes: { id: Theme; name: string; description: string }[] = [ { id: 'dark', name: 'Dark', description: 'Thème sombre par défaut' }, @@ -17,6 +19,50 @@ const themes: { id: Theme; name: string; description: string }[] = [ { id: 'solarized', name: 'Solarized', description: 'Thème Solarized scientifique' }, ]; +// Composant pour l'aperçu du thème +function ThemePreview({ themeId, isSelected }: { themeId: Theme; isSelected: boolean }) { + return ( +
+ {/* Barre de titre */} +
+ + {/* Contenu avec couleurs du thème */} +
+ {/* Ligne de texte */} +
+ + {/* Couleurs d'accent */} +
+
+
+
+
+
+
+ ); +} + export function ThemeSelector() { const { theme, setTheme } = useTheme(); @@ -36,134 +82,19 @@ export function ThemeSelector() {
{themes.map((themeOption) => ( - + ))}
diff --git a/src/components/settings/GeneralSettingsPageClient.tsx b/src/components/settings/GeneralSettingsPageClient.tsx index ff79ff6..9923b22 100644 --- a/src/components/settings/GeneralSettingsPageClient.tsx +++ b/src/components/settings/GeneralSettingsPageClient.tsx @@ -53,6 +53,28 @@ export function GeneralSettingsPageClient({ initialTags }: GeneralSettingsPageCl
+ {/* UI Showcase */} + + +
+
+

+ 🎨 UI Components Showcase +

+

+ Visualisez tous les composants UI disponibles avec différents thèmes +

+
+ + Voir la démo + +
+
+
+ {/* Gestion des tags */} +
+ {/* Header */} +
+

+ 🎨 UI Components Showcase +

+

+ Démonstration de tous les composants UI disponibles +

+
+ + {/* Theme Selector */} +
+
+

+ 🎨 Sélecteur de Thèmes +

+

+ Changez de thème pour voir comment tous les composants s'adaptent +

+
+ +
+
+ +
+
+
+ + {/* Buttons Section */} +
+

+ Buttons +

+ +
+
+

Variants

+
+ + + + + + +
+
+ +
+

Sizes

+
+ + + +
+
+ +
+

States

+
+ + + +
+
+
+
+ + {/* Badges Section */} +
+

+ Badges +

+ +
+
+ Default Badge + Primary Badge + Success Badge + Destructive Badge + Accent Badge + Purple Badge + Yellow Badge + Green Badge + Blue Badge + Gray Badge +
+
+
+ + {/* Alerts Section */} +
+

+ Alerts +

+ +
+ + Information + + Ceci est une alerte par défaut avec des informations importantes. + + + + + Succès + + Opération terminée avec succès ! Toutes les données ont été sauvegardées. + + + + + Erreur + + Une erreur s'est produite lors du traitement de votre demande. + + + + + Attention + + Veuillez vérifier vos informations avant de continuer. + + + + + Conseil + + Astuce : Vous pouvez utiliser les raccourcis clavier pour naviguer plus rapidement. + + +
+
+ + {/* Inputs Section */} +
+

+ Inputs +

+ +
+
+

Types

+
+ + + + + +
+
+ +
+

États

+
+ + + + + setInputValue(e.target.value)} placeholder="État avec valeur" /> +
+
+
+
+ + {/* Cards Section */} +
+

+ Cards +

+ +
+ {/* Standard Cards */} + + + Card Standard + + +

+ Ceci est une carte standard avec header et contenu. +

+
+
+ + + + Card Élevée + + +

+ Cette carte a une ombre plus prononcée. +

+
+
+ + + + Card Bordée + + +

+ Cette carte a une bordure colorée. +

+
+
+ + {/* Styled Cards */} + +

Styled Card Primary

+

+ Carte avec couleur primaire et fond subtil. +

+
+ + +

Styled Card Success

+

+ Carte avec couleur de succès. +

+
+ + +

Styled Card Destructive

+

+ Carte avec couleur destructive. +

+
+
+
+ + {/* Interactive Demo */} +
+

+ Démonstration Interactive +

+ +
+ + + Formulaire d'exemple + + +
+ + +
+ +
+ + +
+ +
+ +
+ Actif + En attente +
+
+ +
+ + +
+
+
+ + + + Notifications + + + + Bienvenue ! + + Votre compte a été créé avec succès. + + + + + Mise à jour disponible + + Une nouvelle version de l'application est disponible. + + + +
+ + +
+
+
+
+
+ + {/* Footer */} +
+

+ Cette page est accessible via /ui-showcase +

+
+
+
+ ); +} diff --git a/src/components/ui/Alert.tsx b/src/components/ui/Alert.tsx new file mode 100644 index 0000000..a06b0c1 --- /dev/null +++ b/src/components/ui/Alert.tsx @@ -0,0 +1,58 @@ +import { HTMLAttributes, forwardRef } from 'react'; +import { cn } from '@/lib/utils'; + +interface AlertProps extends HTMLAttributes { + variant?: 'default' | 'success' | 'destructive' | 'warning' | 'info'; +} + +const Alert = forwardRef( + ({ className, variant = 'default', ...props }, ref) => { + const variants = { + default: 'bg-[var(--card)] text-[var(--foreground)] border border-[var(--border)]', + success: 'bg-[color-mix(in_srgb,var(--success)_10%,transparent)] text-[var(--success)] border border-[color-mix(in_srgb,var(--success)_20%,var(--border))]', + destructive: 'bg-[color-mix(in_srgb,var(--destructive)_10%,transparent)] text-[var(--destructive)] border border-[color-mix(in_srgb,var(--destructive)_20%,var(--border))]', + warning: 'bg-[color-mix(in_srgb,var(--accent)_10%,transparent)] text-[var(--accent)] border border-[color-mix(in_srgb,var(--accent)_20%,var(--border))]', + info: 'bg-[color-mix(in_srgb,var(--primary)_10%,transparent)] text-[var(--primary)] border border-[color-mix(in_srgb,var(--primary)_20%,var(--border))]' + }; + + return ( +
+ ); + } +); + +Alert.displayName = 'Alert'; + +const AlertTitle = forwardRef>( + ({ className, ...props }, ref) => ( +
+ ) +); + +AlertTitle.displayName = 'AlertTitle'; + +const AlertDescription = forwardRef>( + ({ className, ...props }, ref) => ( +
+ ) +); + +AlertDescription.displayName = 'AlertDescription'; + +export { Alert, AlertTitle, AlertDescription }; diff --git a/src/components/ui/Badge.tsx b/src/components/ui/Badge.tsx index aef8aef..527609f 100644 --- a/src/components/ui/Badge.tsx +++ b/src/components/ui/Badge.tsx @@ -1,38 +1,33 @@ import { HTMLAttributes, forwardRef } from 'react'; import { cn } from '@/lib/utils'; -interface BadgeProps extends HTMLAttributes { - variant?: 'default' | 'primary' | 'success' | 'warning' | 'danger' | 'outline'; - size?: 'sm' | 'md'; +interface BadgeProps extends HTMLAttributes { + variant?: 'default' | 'primary' | 'success' | 'destructive' | 'accent' | 'purple' | 'yellow' | 'green' | 'blue' | 'gray'; } -const Badge = forwardRef( - ({ className, variant = 'default', size = 'md', ...props }, ref) => { - const baseStyles = 'inline-flex items-center font-mono font-medium transition-all duration-200'; - +const Badge = forwardRef( + ({ className, variant = 'default', ...props }, ref) => { const variants = { - default: 'bg-[var(--card)] text-[var(--muted-foreground)] border border-[var(--border)]', - primary: 'bg-[var(--primary)]/20 text-[var(--primary)] border border-[var(--primary)]/30', - success: 'bg-[var(--success)]/20 text-[var(--success)] border border-[var(--success)]/30', - warning: 'bg-[var(--accent)]/20 text-[var(--accent)] border border-[var(--accent)]/30', - danger: 'bg-[var(--destructive)]/20 text-[var(--destructive)] border border-[var(--destructive)]/30', - outline: 'bg-transparent text-[var(--muted-foreground)] border border-[var(--border)] hover:bg-[var(--card-hover)] hover:text-[var(--foreground)]' + default: 'bg-[var(--card)] text-[var(--foreground)] border border-[var(--border)]', + primary: 'bg-[color-mix(in_srgb,var(--primary)_10%,transparent)] text-[var(--primary)] border border-[color-mix(in_srgb,var(--primary)_25%,var(--border))]', + success: 'bg-[color-mix(in_srgb,var(--success)_10%,transparent)] text-[var(--success)] border border-[color-mix(in_srgb,var(--success)_25%,var(--border))]', + destructive: 'bg-[color-mix(in_srgb,var(--destructive)_10%,transparent)] text-[var(--destructive)] border border-[color-mix(in_srgb,var(--destructive)_25%,var(--border))]', + accent: 'bg-[color-mix(in_srgb,var(--accent)_10%,transparent)] text-[var(--accent)] border border-[color-mix(in_srgb,var(--accent)_25%,var(--border))]', + purple: 'bg-[color-mix(in_srgb,var(--purple)_10%,transparent)] text-[var(--purple)] border border-[color-mix(in_srgb,var(--purple)_25%,var(--border))]', + yellow: 'bg-[color-mix(in_srgb,var(--yellow)_10%,transparent)] text-[var(--yellow)] border border-[color-mix(in_srgb,var(--yellow)_25%,var(--border))]', + green: 'bg-[color-mix(in_srgb,var(--green)_10%,transparent)] text-[var(--green)] border border-[color-mix(in_srgb,var(--green)_25%,var(--border))]', + blue: 'bg-[color-mix(in_srgb,var(--blue)_10%,transparent)] text-[var(--blue)] border border-[color-mix(in_srgb,var(--blue)_25%,var(--border))]', + gray: 'bg-[color-mix(in_srgb,var(--gray)_10%,transparent)] text-[var(--gray)] border border-[color-mix(in_srgb,var(--gray)_25%,var(--border))]' }; - - const sizes = { - sm: 'px-1.5 py-0.5 text-xs rounded', - md: 'px-2 py-1 text-xs rounded-md' - }; - + return ( - ); @@ -41,4 +36,4 @@ const Badge = forwardRef( Badge.displayName = 'Badge'; -export { Badge }; +export { Badge }; \ No newline at end of file diff --git a/src/components/ui/Button.tsx b/src/components/ui/Button.tsx index 2bab912..615b5d1 100644 --- a/src/components/ui/Button.tsx +++ b/src/components/ui/Button.tsx @@ -2,36 +2,36 @@ import { ButtonHTMLAttributes, forwardRef } from 'react'; import { cn } from '@/lib/utils'; interface ButtonProps extends ButtonHTMLAttributes { - variant?: 'primary' | 'secondary' | 'danger' | 'ghost'; + variant?: 'primary' | 'secondary' | 'ghost' | 'destructive' | 'success' | 'selected'; size?: 'sm' | 'md' | 'lg'; } const Button = forwardRef( ({ className, variant = 'primary', size = 'md', ...props }, ref) => { - const baseStyles = 'inline-flex items-center justify-center font-mono font-medium transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-[var(--background)] disabled:opacity-50 disabled:cursor-not-allowed'; - const variants = { - primary: 'bg-[var(--primary)] hover:bg-[var(--primary)]/80 text-[var(--primary-foreground)] border border-[var(--primary)]/30 shadow-[var(--primary)]/20 shadow-lg hover:shadow-[var(--primary)]/30 focus:ring-[var(--primary)]', - secondary: 'bg-[var(--card)] hover:bg-[var(--card-hover)] text-[var(--foreground)] border border-[var(--border)] shadow-[var(--muted)]/20 shadow-lg hover:shadow-[var(--muted)]/30 focus:ring-[var(--muted)]', - danger: 'bg-[var(--destructive)] hover:bg-[var(--destructive)]/80 text-white border border-[var(--destructive)]/30 shadow-[var(--destructive)]/20 shadow-lg hover:shadow-[var(--destructive)]/30 focus:ring-[var(--destructive)]', - ghost: 'bg-transparent hover:bg-[var(--card)]/50 text-[var(--muted-foreground)] hover:text-[var(--foreground)] border border-[var(--border)]/50 hover:border-[var(--border)] focus:ring-[var(--muted)]' + primary: 'bg-[var(--primary)] text-[var(--primary-foreground)] hover:bg-[color-mix(in_srgb,var(--primary)_90%,transparent)]', + secondary: 'bg-[var(--card)] text-[var(--foreground)] border border-[var(--border)] hover:bg-[var(--card-hover)]', + ghost: 'text-[var(--foreground)] hover:bg-[var(--card-hover)]', + destructive: 'bg-[var(--destructive)] text-white hover:bg-[color-mix(in_srgb,var(--destructive)_90%,transparent)]', + success: 'bg-[var(--success)] text-white hover:bg-[color-mix(in_srgb,var(--success)_90%,transparent)]', + selected: 'bg-[color-mix(in_srgb,var(--primary)_15%,transparent)] text-[var(--foreground)] border border-[var(--primary)] hover:bg-[color-mix(in_srgb,var(--primary)_20%,transparent)]' }; - + const sizes = { - sm: 'px-3 py-1.5 text-xs rounded-md', - md: 'px-4 py-2 text-sm rounded-lg', - lg: 'px-6 py-3 text-base rounded-lg' + sm: 'px-3 py-1.5 text-sm', + md: 'px-4 py-2 text-sm', + lg: 'px-6 py-3 text-base' }; - + return (