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.
This commit is contained in:
Julien Froidefond
2025-09-28 21:08:48 +02:00
parent 9ef23dbddc
commit 0e2eaf1052
12 changed files with 694 additions and 202 deletions

106
UI_COMPONENTS_GUIDE.md Normal file
View File

@@ -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 (
<div className="bg-[var(--card)] border border-[var(--border)] p-4 rounded-lg">
<button className="bg-[var(--primary)] text-[var(--primary-foreground)] px-4 py-2 rounded">
{task.title}
</button>
</div>
);
}
```
## ✅ BON
```tsx
// ✅ Composant métier utilisant les composants UI
import { Card, CardContent, Button } from '@/components/ui';
function TaskCard({ task }) {
return (
<Card>
<CardContent>
<Button variant="primary">
{task.title}
</Button>
</CardContent>
</Card>
);
}
```
## 📦 Composants UI Disponibles
### Button
```tsx
<Button variant="primary" size="md">Action</Button>
<Button variant="secondary">Secondaire</Button>
<Button variant="destructive">Supprimer</Button>
<Button variant="ghost">Ghost</Button>
```
### Badge
```tsx
<Badge variant="primary">Tag</Badge>
<Badge variant="success">Succès</Badge>
<Badge variant="destructive">Erreur</Badge>
```
### Alert
```tsx
<Alert variant="success">
<AlertTitle>Succès</AlertTitle>
<AlertDescription>Opération réussie</AlertDescription>
</Alert>
```
### Input
```tsx
<Input placeholder="Saisir..." />
<Input variant="error" placeholder="Erreur" />
```
### StyledCard
```tsx
<StyledCard variant="outline" color="primary">
Contenu avec style coloré
</StyledCard>
```
## 🔄 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

View File

@@ -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 */

View File

@@ -0,0 +1,5 @@
import { UIShowcaseClient } from '@/components/ui-showcase/UIShowcaseClient';
export default function UIShowcasePage() {
return <UIShowcaseClient />;
}

View File

@@ -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 (
<div
className={`w-16 h-12 rounded-lg border-2 overflow-hidden ${themeId}`}
style={{
borderColor: isSelected ? 'var(--primary)' : 'var(--border)',
backgroundColor: 'var(--background)'
}}
>
{/* Barre de titre */}
<div
className="h-3 w-full"
style={{ backgroundColor: 'var(--card)' }}
/>
{/* Contenu avec couleurs du thème */}
<div className="p-1 h-9 flex flex-col gap-0.5">
{/* Ligne de texte */}
<div
className="h-1 rounded-sm"
style={{ backgroundColor: 'var(--foreground)' }}
/>
{/* Couleurs d'accent */}
<div className="flex gap-0.5">
<div
className="h-1 flex-1 rounded-sm"
style={{ backgroundColor: 'var(--primary)' }}
/>
<div
className="h-1 flex-1 rounded-sm"
style={{ backgroundColor: 'var(--accent)' }}
/>
<div
className="h-1 flex-1 rounded-sm"
style={{ backgroundColor: 'var(--success)' }}
/>
</div>
</div>
</div>
);
}
export function ThemeSelector() {
const { theme, setTheme } = useTheme();
@@ -36,134 +82,19 @@ export function ThemeSelector() {
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3">
{themes.map((themeOption) => (
<button
<Button
key={themeOption.id}
onClick={() => setTheme(themeOption.id)}
className={`
p-4 rounded-lg border text-left transition-all duration-200 group
${theme === themeOption.id
? 'border-[var(--primary)] bg-[color-mix(in_srgb,var(--primary)_15%,transparent)] shadow-lg shadow-[var(--primary)]/20'
: 'border-[var(--border)] hover:border-[var(--primary)] hover:bg-[color-mix(in_srgb,var(--primary)_8%,transparent)] hover:shadow-md'
}
`}
variant={theme === themeOption.id ? 'selected' : 'secondary'}
className="p-4 h-auto text-left justify-start"
>
<div className="flex items-start gap-3">
{/* Aperçu du thème */}
<div className="flex-shrink-0">
<div
className="w-16 h-12 rounded-lg border-2 overflow-hidden"
style={{
borderColor: theme === themeOption.id ? 'var(--primary)' : 'var(--border)',
backgroundColor: themeOption.id === 'light' ? '#f1f5f9' :
themeOption.id === 'dark' ? '#1e293b' :
themeOption.id === 'dracula' ? '#282a36' :
themeOption.id === 'monokai' ? '#272822' :
themeOption.id === 'nord' ? '#2e3440' :
themeOption.id === 'gruvbox' ? '#282828' :
themeOption.id === 'tokyo_night' ? '#1a1b26' :
themeOption.id === 'catppuccin' ? '#1e1e2e' :
themeOption.id === 'rose_pine' ? '#191724' :
themeOption.id === 'one_dark' ? '#282c34' :
themeOption.id === 'material' ? '#121212' :
'#002b36'
}}
>
{/* Barre de titre */}
<div
className="h-3 w-full"
style={{
backgroundColor: themeOption.id === 'light' ? '#ffffff' :
themeOption.id === 'dark' ? '#334155' :
themeOption.id === 'dracula' ? '#44475a' :
themeOption.id === 'monokai' ? '#3e3d32' :
themeOption.id === 'nord' ? '#3b4252' :
themeOption.id === 'gruvbox' ? '#3c3836' :
themeOption.id === 'tokyo_night' ? '#24283b' :
themeOption.id === 'catppuccin' ? '#313244' :
themeOption.id === 'rose_pine' ? '#26233a' :
themeOption.id === 'one_dark' ? '#3e4451' :
themeOption.id === 'material' ? '#1e1e1e' :
'#073642'
}}
<ThemePreview
themeId={themeOption.id}
isSelected={theme === themeOption.id}
/>
{/* Contenu avec couleurs du thème */}
<div className="p-1 h-9 flex flex-col gap-0.5">
{/* Ligne de texte */}
<div
className="h-1 rounded-sm"
style={{
backgroundColor: themeOption.id === 'light' ? '#0f172a' :
themeOption.id === 'dark' ? '#f1f5f9' :
themeOption.id === 'dracula' ? '#f8f8f2' :
themeOption.id === 'monokai' ? '#f8f8f2' :
themeOption.id === 'nord' ? '#d8dee9' :
themeOption.id === 'gruvbox' ? '#ebdbb2' :
themeOption.id === 'tokyo_night' ? '#a9b1d6' :
themeOption.id === 'catppuccin' ? '#cdd6f4' :
themeOption.id === 'rose_pine' ? '#e0def4' :
themeOption.id === 'one_dark' ? '#abb2bf' :
themeOption.id === 'material' ? '#ffffff' :
'#93a1a1'
}}
/>
{/* Couleurs d'accent */}
<div className="flex gap-0.5">
<div
className="h-1 flex-1 rounded-sm"
style={{
backgroundColor: themeOption.id === 'light' ? '#0891b2' :
themeOption.id === 'dark' ? '#06b6d4' :
themeOption.id === 'dracula' ? '#ff79c6' :
themeOption.id === 'monokai' ? '#f92672' :
themeOption.id === 'nord' ? '#88c0d0' :
themeOption.id === 'gruvbox' ? '#fe8019' :
themeOption.id === 'tokyo_night' ? '#7aa2f7' :
themeOption.id === 'catppuccin' ? '#cba6f7' :
themeOption.id === 'rose_pine' ? '#c4a7e7' :
themeOption.id === 'one_dark' ? '#61afef' :
themeOption.id === 'material' ? '#bb86fc' :
'#268bd2'
}}
/>
<div
className="h-1 flex-1 rounded-sm"
style={{
backgroundColor: themeOption.id === 'light' ? '#d97706' :
themeOption.id === 'dark' ? '#f59e0b' :
themeOption.id === 'dracula' ? '#ffb86c' :
themeOption.id === 'monokai' ? '#fd971f' :
themeOption.id === 'nord' ? '#d08770' :
themeOption.id === 'gruvbox' ? '#fabd2f' :
themeOption.id === 'tokyo_night' ? '#ff9e64' :
themeOption.id === 'catppuccin' ? '#fab387' :
themeOption.id === 'rose_pine' ? '#f6c177' :
themeOption.id === 'one_dark' ? '#e06c75' :
themeOption.id === 'material' ? '#ffab40' :
'#b58900'
}}
/>
<div
className="h-1 flex-1 rounded-sm"
style={{
backgroundColor: themeOption.id === 'light' ? '#059669' :
themeOption.id === 'dark' ? '#10b981' :
themeOption.id === 'dracula' ? '#50fa7b' :
themeOption.id === 'monokai' ? '#a6e22e' :
themeOption.id === 'nord' ? '#a3be8c' :
themeOption.id === 'gruvbox' ? '#b8bb26' :
themeOption.id === 'tokyo_night' ? '#9ece6a' :
themeOption.id === 'catppuccin' ? '#a6e3a1' :
themeOption.id === 'rose_pine' ? '#9ccfd8' :
themeOption.id === 'one_dark' ? '#98c379' :
themeOption.id === 'material' ? '#4caf50' :
'#859900'
}}
/>
</div>
</div>
</div>
</div>
<div className="flex-1 min-w-0">
@@ -180,7 +111,7 @@ export function ThemeSelector() {
)}
</div>
</div>
</button>
</Button>
))}
</div>
</div>

View File

@@ -53,6 +53,28 @@ export function GeneralSettingsPageClient({ initialTags }: GeneralSettingsPageCl
<ThemeSelector />
</div>
{/* UI Showcase */}
<Card>
<CardContent className="p-6">
<div className="flex items-center justify-between">
<div>
<h3 className="text-lg font-medium text-[var(--foreground)] mb-2">
🎨 UI Components Showcase
</h3>
<p className="text-sm text-[var(--muted-foreground)]">
Visualisez tous les composants UI disponibles avec différents thèmes
</p>
</div>
<Link
href="/ui-showcase"
className="inline-flex items-center px-4 py-2 bg-[var(--primary)] text-[var(--primary-foreground)] rounded-md hover:bg-[color-mix(in_srgb,var(--primary)_90%,transparent)] transition-colors font-medium"
>
Voir la démo
</Link>
</div>
</CardContent>
</Card>
{/* Gestion des tags */}
<TagsManagement
tags={tags}

View File

@@ -0,0 +1,327 @@
'use client';
import { useState } from 'react';
import { Button } from '@/components/ui/Button';
import { Badge } from '@/components/ui/Badge';
import { Alert, AlertTitle, AlertDescription } from '@/components/ui/Alert';
import { Input } from '@/components/ui/Input';
import { StyledCard } from '@/components/ui/StyledCard';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card';
import { ThemeSelector } from '@/components/ThemeSelector';
export function UIShowcaseClient() {
const [inputValue, setInputValue] = useState('');
return (
<div className="min-h-screen bg-[var(--background)] p-8">
<div className="max-w-6xl mx-auto space-y-16">
{/* Header */}
<div className="text-center space-y-6">
<h1 className="text-4xl font-mono font-bold text-[var(--foreground)]">
🎨 UI Components Showcase
</h1>
<p className="text-lg text-[var(--muted-foreground)]">
Démonstration de tous les composants UI disponibles
</p>
</div>
{/* Theme Selector */}
<section className="space-y-8">
<div className="text-center">
<h2 className="text-2xl font-mono font-semibold text-[var(--foreground)] mb-6">
🎨 Sélecteur de Thèmes
</h2>
<p className="text-[var(--muted-foreground)] mb-8">
Changez de thème pour voir comment tous les composants s'adaptent
</p>
</div>
<div className="max-w-4xl mx-auto">
<div className="bg-[var(--card)]/30 border border-[var(--border)]/50 rounded-lg p-6 backdrop-blur-sm">
<ThemeSelector />
</div>
</div>
</section>
{/* Buttons Section */}
<section className="space-y-8">
<h2 className="text-2xl font-mono font-semibold text-[var(--foreground)] border-b border-[var(--border)] pb-3">
Buttons
</h2>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
<div className="space-y-6">
<h3 className="text-lg font-medium text-[var(--foreground)]">Variants</h3>
<div className="space-y-4">
<Button variant="primary">Primary Button</Button>
<Button variant="secondary">Secondary Button</Button>
<Button variant="ghost">Ghost Button</Button>
<Button variant="destructive">Destructive Button</Button>
<Button variant="success">Success Button</Button>
<Button variant="selected">Selected Button</Button>
</div>
</div>
<div className="space-y-6">
<h3 className="text-lg font-medium text-[var(--foreground)]">Sizes</h3>
<div className="space-y-4">
<Button size="sm">Small Button</Button>
<Button size="md">Medium Button</Button>
<Button size="lg">Large Button</Button>
</div>
</div>
<div className="space-y-6">
<h3 className="text-lg font-medium text-[var(--foreground)]">States</h3>
<div className="space-y-4">
<Button>Normal State</Button>
<Button disabled>Disabled State</Button>
<Button className="opacity-50">Loading State</Button>
</div>
</div>
</div>
</section>
{/* Badges Section */}
<section className="space-y-8">
<h2 className="text-2xl font-mono font-semibold text-[var(--foreground)] border-b border-[var(--border)] pb-3">
Badges
</h2>
<div className="space-y-6">
<div className="flex flex-wrap gap-4">
<Badge variant="default">Default Badge</Badge>
<Badge variant="primary">Primary Badge</Badge>
<Badge variant="success">Success Badge</Badge>
<Badge variant="destructive">Destructive Badge</Badge>
<Badge variant="accent">Accent Badge</Badge>
<Badge variant="purple">Purple Badge</Badge>
<Badge variant="yellow">Yellow Badge</Badge>
<Badge variant="green">Green Badge</Badge>
<Badge variant="blue">Blue Badge</Badge>
<Badge variant="gray">Gray Badge</Badge>
</div>
</div>
</section>
{/* Alerts Section */}
<section className="space-y-8">
<h2 className="text-2xl font-mono font-semibold text-[var(--foreground)] border-b border-[var(--border)] pb-3">
Alerts
</h2>
<div className="space-y-6">
<Alert variant="default">
<AlertTitle>Information</AlertTitle>
<AlertDescription>
Ceci est une alerte par défaut avec des informations importantes.
</AlertDescription>
</Alert>
<Alert variant="success">
<AlertTitle>Succès</AlertTitle>
<AlertDescription>
Opération terminée avec succès ! Toutes les données ont été sauvegardées.
</AlertDescription>
</Alert>
<Alert variant="destructive">
<AlertTitle>Erreur</AlertTitle>
<AlertDescription>
Une erreur s'est produite lors du traitement de votre demande.
</AlertDescription>
</Alert>
<Alert variant="warning">
<AlertTitle>Attention</AlertTitle>
<AlertDescription>
Veuillez vérifier vos informations avant de continuer.
</AlertDescription>
</Alert>
<Alert variant="info">
<AlertTitle>Conseil</AlertTitle>
<AlertDescription>
Astuce : Vous pouvez utiliser les raccourcis clavier pour naviguer plus rapidement.
</AlertDescription>
</Alert>
</div>
</section>
{/* Inputs Section */}
<section className="space-y-8">
<h2 className="text-2xl font-mono font-semibold text-[var(--foreground)] border-b border-[var(--border)] pb-3">
Inputs
</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
<div className="space-y-6">
<h3 className="text-lg font-medium text-[var(--foreground)]">Types</h3>
<div className="space-y-4">
<Input placeholder="Texte normal" />
<Input type="email" placeholder="Adresse email" />
<Input type="password" placeholder="Mot de passe" />
<Input type="number" placeholder="Nombre" />
<Input type="search" placeholder="Rechercher..." />
</div>
</div>
<div className="space-y-6">
<h3 className="text-lg font-medium text-[var(--foreground)]">États</h3>
<div className="space-y-4">
<Input placeholder="État normal" />
<Input placeholder="État focus" autoFocus />
<Input placeholder="État désactivé" disabled />
<Input variant="error" placeholder="État erreur" />
<Input value={inputValue} onChange={(e) => setInputValue(e.target.value)} placeholder="État avec valeur" />
</div>
</div>
</div>
</section>
{/* Cards Section */}
<section className="space-y-6">
<h2 className="text-2xl font-mono font-semibold text-[var(--foreground)] border-b border-[var(--border)] pb-2">
Cards
</h2>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{/* Standard Cards */}
<Card>
<CardHeader>
<CardTitle>Card Standard</CardTitle>
</CardHeader>
<CardContent>
<p className="text-[var(--muted-foreground)]">
Ceci est une carte standard avec header et contenu.
</p>
</CardContent>
</Card>
<Card variant="elevated">
<CardHeader>
<CardTitle>Card Élevée</CardTitle>
</CardHeader>
<CardContent>
<p className="text-[var(--muted-foreground)]">
Cette carte a une ombre plus prononcée.
</p>
</CardContent>
</Card>
<Card variant="bordered">
<CardHeader>
<CardTitle>Card Bordée</CardTitle>
</CardHeader>
<CardContent>
<p className="text-[var(--muted-foreground)]">
Cette carte a une bordure colorée.
</p>
</CardContent>
</Card>
{/* Styled Cards */}
<StyledCard color="primary" className="p-4">
<h3 className="font-medium mb-2">Styled Card Primary</h3>
<p className="text-sm opacity-80">
Carte avec couleur primaire et fond subtil.
</p>
</StyledCard>
<StyledCard color="success" className="p-4">
<h3 className="font-medium mb-2">Styled Card Success</h3>
<p className="text-sm opacity-80">
Carte avec couleur de succès.
</p>
</StyledCard>
<StyledCard color="destructive" className="p-4">
<h3 className="font-medium mb-2">Styled Card Destructive</h3>
<p className="text-sm opacity-80">
Carte avec couleur destructive.
</p>
</StyledCard>
</div>
</section>
{/* Interactive Demo */}
<section className="space-y-6">
<h2 className="text-2xl font-mono font-semibold text-[var(--foreground)] border-b border-[var(--border)] pb-2">
Démonstration Interactive
</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<Card>
<CardHeader>
<CardTitle>Formulaire d'exemple</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div>
<label className="block text-sm font-medium text-[var(--foreground)] mb-2">
Nom d'utilisateur
</label>
<Input placeholder="Entrez votre nom" />
</div>
<div>
<label className="block text-sm font-medium text-[var(--foreground)] mb-2">
Email
</label>
<Input type="email" placeholder="votre@email.com" />
</div>
<div>
<label className="block text-sm font-medium text-[var(--foreground)] mb-2">
Statut
</label>
<div className="flex gap-2">
<Badge variant="success">Actif</Badge>
<Badge variant="gray">En attente</Badge>
</div>
</div>
<div className="flex gap-3 pt-4">
<Button variant="primary">Enregistrer</Button>
<Button variant="secondary">Annuler</Button>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Notifications</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<Alert variant="success">
<AlertTitle>Bienvenue !</AlertTitle>
<AlertDescription>
Votre compte a é créé avec succès.
</AlertDescription>
</Alert>
<Alert variant="warning">
<AlertTitle>Mise à jour disponible</AlertTitle>
<AlertDescription>
Une nouvelle version de l'application est disponible.
</AlertDescription>
</Alert>
<div className="flex gap-2">
<Button size="sm">Voir les détails</Button>
<Button size="sm" variant="ghost">Ignorer</Button>
</div>
</CardContent>
</Card>
</div>
</section>
{/* Footer */}
<div className="text-center pt-8 border-t border-[var(--border)]">
<p className="text-[var(--muted-foreground)]">
Cette page est accessible via <code className="bg-[var(--card)] px-2 py-1 rounded text-sm">/ui-showcase</code>
</p>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,58 @@
import { HTMLAttributes, forwardRef } from 'react';
import { cn } from '@/lib/utils';
interface AlertProps extends HTMLAttributes<HTMLDivElement> {
variant?: 'default' | 'success' | 'destructive' | 'warning' | 'info';
}
const Alert = forwardRef<HTMLDivElement, AlertProps>(
({ 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 (
<div
ref={ref}
className={cn(
'relative w-full rounded-lg border p-4',
variants[variant],
className
)}
{...props}
/>
);
}
);
Alert.displayName = 'Alert';
const AlertTitle = forwardRef<HTMLParagraphElement, HTMLAttributes<HTMLHeadingElement>>(
({ className, ...props }, ref) => (
<h5
ref={ref}
className={cn('mb-1 font-medium leading-none tracking-tight', className)}
{...props}
/>
)
);
AlertTitle.displayName = 'AlertTitle';
const AlertDescription = forwardRef<HTMLParagraphElement, HTMLAttributes<HTMLParagraphElement>>(
({ className, ...props }, ref) => (
<div
ref={ref}
className={cn('text-sm [&_p]:leading-relaxed', className)}
{...props}
/>
)
);
AlertDescription.displayName = 'AlertDescription';
export { Alert, AlertTitle, AlertDescription };

View File

@@ -1,38 +1,33 @@
import { HTMLAttributes, forwardRef } from 'react';
import { cn } from '@/lib/utils';
interface BadgeProps extends HTMLAttributes<HTMLSpanElement> {
variant?: 'default' | 'primary' | 'success' | 'warning' | 'danger' | 'outline';
size?: 'sm' | 'md';
interface BadgeProps extends HTMLAttributes<HTMLDivElement> {
variant?: 'default' | 'primary' | 'success' | 'destructive' | 'accent' | 'purple' | 'yellow' | 'green' | 'blue' | 'gray';
}
const Badge = forwardRef<HTMLSpanElement, BadgeProps>(
({ className, variant = 'default', size = 'md', ...props }, ref) => {
const baseStyles = 'inline-flex items-center font-mono font-medium transition-all duration-200';
const Badge = forwardRef<HTMLDivElement, BadgeProps>(
({ 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)]'
};
const sizes = {
sm: 'px-1.5 py-0.5 text-xs rounded',
md: 'px-2 py-1 text-xs rounded-md'
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))]'
};
return (
<span
<div
ref={ref}
className={cn(
baseStyles,
'inline-flex items-center rounded-md px-2.5 py-0.5 text-xs font-medium transition-colors',
variants[variant],
sizes[size],
className
)}
ref={ref}
{...props}
/>
);

View File

@@ -2,36 +2,36 @@ import { ButtonHTMLAttributes, forwardRef } from 'react';
import { cn } from '@/lib/utils';
interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
variant?: 'primary' | 'secondary' | 'danger' | 'ghost';
variant?: 'primary' | 'secondary' | 'ghost' | 'destructive' | 'success' | 'selected';
size?: 'sm' | 'md' | 'lg';
}
const Button = forwardRef<HTMLButtonElement, ButtonProps>(
({ 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 (
<button
ref={ref}
className={cn(
baseStyles,
'inline-flex items-center justify-center rounded-md font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--primary)] focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50',
variants[variant],
sizes[size],
className
)}
ref={ref}
{...props}
/>
);

View File

@@ -2,39 +2,27 @@ import { InputHTMLAttributes, forwardRef } from 'react';
import { cn } from '@/lib/utils';
interface InputProps extends InputHTMLAttributes<HTMLInputElement> {
label?: string;
error?: string;
variant?: 'default' | 'error';
}
const Input = forwardRef<HTMLInputElement, InputProps>(
({ className, label, error, ...props }, ref) => {
({ className, variant = 'default', type, ...props }, ref) => {
const variants = {
default: 'border border-[var(--border)]/50 bg-[var(--input)] text-[var(--foreground)] placeholder:text-[var(--muted-foreground)] focus:border-[var(--primary)]/70 focus:ring-1 focus:ring-[var(--primary)]/20',
error: 'border border-[var(--destructive)]/50 bg-[var(--input)] text-[var(--foreground)] placeholder:text-[var(--muted-foreground)] focus:border-[var(--destructive)]/70 focus:ring-1 focus:ring-[var(--destructive)]/20'
};
return (
<div className="space-y-2">
{label && (
<label className="block text-sm font-mono font-medium text-[var(--muted-foreground)] uppercase tracking-wider">
{label}
</label>
)}
<input
type={type}
className={cn(
'w-full px-3 py-2 bg-[var(--input)] border border-[var(--border)]/50 rounded-lg',
'text-[var(--foreground)] font-mono text-sm placeholder-[var(--muted-foreground)]',
'focus:outline-none focus:ring-2 focus:ring-[var(--primary)]/50 focus:border-[var(--primary)]/50',
'hover:border-[var(--border)] transition-all duration-200',
'backdrop-blur-sm',
error && 'border-[var(--destructive)]/50 focus:ring-[var(--destructive)]/50 focus:border-[var(--destructive)]/50',
'flex h-10 w-full rounded-md px-3 py-2 text-sm file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50 transition-colors',
variants[variant],
className
)}
ref={ref}
{...props}
/>
{error && (
<p className="text-xs font-mono text-[var(--destructive)] flex items-center gap-1">
<span className="text-[var(--destructive)]"></span>
{error}
</p>
)}
</div>
);
}
);

View File

@@ -0,0 +1,49 @@
import { HTMLAttributes, forwardRef } from 'react';
import { cn } from '@/lib/utils';
interface StyledCardProps extends HTMLAttributes<HTMLDivElement> {
variant?: 'default' | 'outline' | 'elevated' | 'bordered' | 'column';
color?: 'default' | 'primary' | 'success' | 'destructive' | 'accent' | 'purple' | 'yellow' | 'green' | 'blue' | 'gray';
}
const StyledCard = forwardRef<HTMLDivElement, StyledCardProps>(
({ className, variant = 'default', color = 'default', ...props }, ref) => {
const variants = {
default: 'bg-[var(--card)]/50 border border-[var(--border)]/50',
outline: 'bg-transparent border border-[var(--border)]',
elevated: 'bg-[var(--card)]/80 border border-[var(--border)]/50 shadow-lg shadow-[var(--card)]/20',
bordered: 'bg-[var(--card)]/50 border border-[var(--primary)]/30 shadow-[var(--primary)]/10 shadow-lg',
column: 'bg-[var(--card-column)] border border-[var(--border)]/50 shadow-lg shadow-[var(--card)]/20'
};
const colors = {
default: '',
primary: 'bg-[color-mix(in_srgb,var(--primary)_8%,transparent)] border-[color-mix(in_srgb,var(--primary)_25%,var(--border))] text-[var(--primary)]',
success: 'bg-[color-mix(in_srgb,var(--success)_8%,transparent)] border-[color-mix(in_srgb,var(--success)_25%,var(--border))] text-[var(--success)]',
destructive: 'bg-[color-mix(in_srgb,var(--destructive)_8%,transparent)] border-[color-mix(in_srgb,var(--destructive)_25%,var(--border))] text-[var(--destructive)]',
accent: 'bg-[color-mix(in_srgb,var(--accent)_8%,transparent)] border-[color-mix(in_srgb,var(--accent)_25%,var(--border))] text-[var(--accent)]',
purple: 'bg-[color-mix(in_srgb,var(--purple)_8%,transparent)] border-[color-mix(in_srgb,var(--purple)_25%,var(--border))] text-[var(--purple)]',
yellow: 'bg-[color-mix(in_srgb,var(--yellow)_8%,transparent)] border-[color-mix(in_srgb,var(--yellow)_25%,var(--border))] text-[var(--yellow)]',
green: 'bg-[color-mix(in_srgb,var(--green)_8%,transparent)] border-[color-mix(in_srgb,var(--green)_25%,var(--border))] text-[var(--green)]',
blue: 'bg-[color-mix(in_srgb,var(--blue)_8%,transparent)] border-[color-mix(in_srgb,var(--blue)_25%,var(--border))] text-[var(--blue)]',
gray: 'bg-[color-mix(in_srgb,var(--gray)_8%,transparent)] border-[color-mix(in_srgb,var(--gray)_25%,var(--border))] text-[var(--gray)]'
};
return (
<div
ref={ref}
className={cn(
'rounded-lg backdrop-blur-sm transition-all duration-200',
variants[variant],
colors[color],
className
)}
{...props}
/>
);
}
);
StyledCard.displayName = 'StyledCard';
export { StyledCard };

View File

@@ -0,0 +1,11 @@
// Composants UI de base
export { Button } from './Button';
export { Badge } from './Badge';
export { Alert, AlertTitle, AlertDescription } from './Alert';
export { Input } from './Input';
export { StyledCard } from './StyledCard';
// Composants existants
export { Card, CardHeader, CardTitle, CardContent, CardFooter } from './Card';
export { Header } from './Header';
export { FontSizeToggle } from './FontSizeToggle';