---
name: ui-components
description: |
  Génère des composants UI SmartStack standardisés.
  Utiliser ce skill quand:
  - Création de page React (.tsx) dans src/pages/
  - Création de composant React dans src/components/
  - L'utilisateur demande de créer des cards, grilles, tableaux, ou Kanban
  - Claude détecte la création d'une liste d'entités avec affichage
  - L'utilisateur mentionne "card", "grille", "tableau", "liste", "kanban"
  - L'utilisateur demande des tooltips ou infobulles
  - Création d'une page avec affichage d'entités
  - Gestion des états désactivés avec messages explicatifs
  Scope: Pages, Components, Cards, Tables, Grids, Kanban boards, Tooltips
---

# Skill UI Components SmartStack

> **Synergie Skill/Composant:**
> - **Skill** (`.claude/skills/ui-components/`) → Invocation automatique par Claude
> - **Composant** (`components/ui/EntityCard.tsx`) → Source de vérité du rendu
> - Templates et patterns dans ce fichier

## QUAND CE SKILL S'ACTIVE

Claude invoque automatiquement ce skill quand il détecte :

| Déclencheur | Exemple |
|-------------|---------|
| **Création page React** | Écriture de fichier dans `src/pages/**/*.tsx` |
| **Création composant** | Écriture de fichier dans `src/components/**/*.tsx` |
| Création de liste | "Affiche les produits en cards" |
| Nouveau module avec UI | "Crée un module avec grille de cards" |
| Refactoring UI | "Uniformise les cards de cette page" |
| Mots-clés | "card", "grille", "tableau", "kanban", "liste" |
| Tooltips/Infobulles | "Ajoute un tooltip sur ce bouton" |
| États désactivés | "Désactive le bouton avec un message explicatif" |
| Permissions UI | "Affiche pourquoi l'utilisateur ne peut pas cliquer" |

---

## DESIGN SYSTEM DE RÉFÉRENCE

**Page de référence:** `/platform/administration/ai/settings`

### Anatomie d'une Card Standard

```
┌─────────────────────────────────────────────────────────────────────────┐
│ HEADER (bg-accent-50)                                                   │
│ ┌─────┐                                                      [Badge]   │
│ │  O  │  Titre Principal                                               │
│ └─────┘  code-slug                                                     │
├─────────────────────────────────────────────────────────────────────────┤
│ BODY                                                                    │
│                                                                         │
│ Description du contenu sur une ou plusieurs                            │
│ lignes avec texte secondaire.                                          │
│                                                                         │
│ 15 modèle(s)                                                           │
│                                                                         │
│ ─────────────────────────────────────────────                          │
│ ↗ Site officiel                                                        │
│ 📖 Documentation API                                                   │
│                                                                         │
│ ┌─────────────────────────────────────────────────────────────────────┐│
│ │           Obtenir une clé API                    (primary)         ││
│ └─────────────────────────────────────────────────────────────────────┘│
│ ┌─────────────────────────────────────────────────────────────────────┐│
│ │       Obtenir une clé API Admin                  (secondary)       ││
│ └─────────────────────────────────────────────────────────────────────┘│
└─────────────────────────────────────────────────────────────────────────┘
```

### Caractéristiques du Design

| Élément | Style |
|---------|-------|
| **Card** | `bg-[var(--bg-card)]` avec `border-2 border-[var(--color-accent-200)]` |
| **Header** | Section colorée séparée `bg-[var(--color-accent-50)]` |
| **Avatar** | Carré arrondi `rounded-lg` (PAS cercle), `shadow-lg` |
| **Subtitle** | Utilise `<code>` pour le code/slug |
| **Badge** | Icône avec tooltip au hover, dans le header |
| **Links** | Séparés par `border-t`, icône + texte |
| **Actions** | Full-width, variants primary/secondary |
| **Spacer** | `flex-1 min-h-4` pour push buttons en bas |

### Couleurs d'Accent

```
--color-accent-50    → Header background (light)
--color-accent-200   → Border color
--color-accent-400   → Border hover
--color-accent-500   → Button primary background
--color-accent-600   → Button primary hover
--color-accent-700   → Button secondary background / Badge icon
--color-accent-800   → Button secondary hover / Dark mode border
--color-accent-900   → Dark mode header background
```

---

## COMPOSANT OBLIGATOIRE: EntityCard

**Fichier:** `web/smartstack-web/src/components/ui/EntityCard.tsx`

### RÈGLE ABSOLUE

> **TOUJOURS utiliser `EntityCard` pour les cards d'entités.**
> Ne JAMAIS créer de cards custom avec des divs manuels.

### Import Standard

```typescript
import { EntityCard, ProviderCard, TemplateCard } from '@/components/ui/EntityCard';
```

### Usage EntityCard (Générique)

```tsx
<EntityCard
  avatar={{ letter: 'O', color: '#10a37f' }}
  title="OpenAI"
  subtitle="openai"
  description="OpenAI GPT models (GPT-4, GPT-4o, GPT-3.5)"
  stats="15 modèle(s)"
  badge={{ icon: Shield, tooltip: 'API Admin supportée' }}
  links={[
    { icon: ExternalLink, label: 'Site officiel', href: 'https://openai.com' },
    { icon: BookOpen, label: 'Documentation API', href: 'https://platform.openai.com/docs' },
  ]}
  actions={[
    { label: 'Obtenir une clé API', href: 'https://...', variant: 'primary', icon: Key },
    { label: 'Obtenir une clé API Admin', href: 'https://...', variant: 'secondary', icon: Shield },
  ]}
/>
```

### Usage ProviderCard (AI Providers)

```tsx
<ProviderCard
  name="OpenAI"
  code="openai"
  description="OpenAI GPT models (GPT-4, GPT-4o, GPT-3.5)"
  modelCount={15}
  color="#10a37f"
  websiteUrl="https://openai.com"
  docsUrl="https://platform.openai.com/docs"
  apiKeyUrl="https://platform.openai.com/api-keys"
  adminApiKeyUrl="https://platform.openai.com/organization/admin-keys"
  hasAdminKey
  badgeIcon={Shield}
  badgeTooltip="API Admin supportée"
/>
```

### Usage TemplateCard (Templates)

```tsx
<TemplateCard
  name="Welcome Email"
  code="welcome"
  category="Transactional"
  isActive
  isSystem
  icon={Mail}
  iconColor="var(--color-accent-500)"
  translationsCount={3}
  onClick={() => navigate('/...')}
  onEdit={() => navigate('/edit')}
  onDelete={() => handleDelete()}
  labels={{
    activeLabel: t('emailTemplates.active'),
    inactiveLabel: t('emailTemplates.inactive'),
    systemLabel: 'System',
    editLabel: t('emailTemplates.edit'),
    deleteLabel: t('emailTemplates.delete'),
  }}
/>
```

### TemplateCard Props

| Prop | Type | Description |
|------|------|-------------|
| `name` | `string` | Nom du template (requis) |
| `code` | `string` | Code/slug du template (requis) |
| `category` | `string` | Catégorie affichée en tag |
| `isActive` | `boolean` | État actif/inactif (badge dans header) |
| `isSystem` | `boolean` | Template système (non supprimable) |
| `icon` | `ElementType` | Icône Lucide pour l'avatar |
| `iconColor` | `string` | Couleur de l'avatar (CSS) |
| `translationsCount` | `number` | Nombre de traductions |
| `onClick` | `() => void` | Click sur la card |
| `onEdit` | `() => void` | Bouton éditer |
| `onDelete` | `() => void` | Bouton supprimer (masqué si isSystem) |
| `labels` | `object` | Labels i18n pour tous les textes |

---

## PROPS RÉFÉRENCE

### EntityCard Props

| Prop | Type | Description |
|------|------|-------------|
| `avatar` | `{ letter, color, imageUrl? }` | Avatar carré arrondi |
| `title` | `string` | Titre principal (requis) |
| `subtitle` | `string` | Code/slug en gris (affiché en `<code>`) |
| `description` | `string \| ReactNode` | Description (line-clamp-2) |
| `stats` | `string \| ReactNode` | Statistique (nombre en bold) |
| `badge` | `{ icon?, tooltip?, color? }` | Badge dans le header |
| `links` | `Array<{ icon, label, href?, onClick? }>` | Liens avec icônes |
| `actions` | `Array<{ label, href?, onClick?, variant, icon?, disabled?, loading? }>` | Boutons d'action |
| `tags` | `Array<{ label, variant, onClick? }>` | Tags/catégories |
| `onClick` | `() => void` | Click sur la card |
| `customHeader` | `ReactNode` | Remplace le header par défaut |
| `customBody` | `ReactNode` | Remplace le body par défaut |
| `customFooter` | `ReactNode` | Remplace le footer par défaut |

### Action Props

| Prop | Type | Description |
|------|------|-------------|
| `label` | `string` | Texte du bouton |
| `href` | `string` | URL (rend un `<a>` au lieu de `<button>`) |
| `onClick` | `() => void` | Handler de click |
| `variant` | `'primary' \| 'secondary' \| 'ghost'` | Style du bouton |
| `icon` | `ElementType` | Icône Lucide |
| `disabled` | `boolean` | État désactivé |
| `loading` | `boolean` | État de chargement |

### Action Variants

| Variant | Style |
|---------|-------|
| `primary` | `bg-accent-500 hover:bg-accent-600 text-white` |
| `secondary` | `bg-accent-700 hover:bg-accent-800 text-white` |
| `ghost` | `border border-border-color bg-transparent` |

---

## PATTERNS DE GRILLE

### Grille Responsive Standard

```tsx
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
  {items.map(item => (
    <EntityCard key={item.id} {...mapToCardProps(item)} />
  ))}
</div>
```

### Grille avec État Vide

```tsx
{items.length === 0 ? (
  <div className="col-span-full text-center py-12">
    <Package className="w-12 h-12 mx-auto mb-4 opacity-50" />
    <p className="text-[var(--text-secondary)]">Aucun élément trouvé</p>
  </div>
) : (
  <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
    {items.map(item => (
      <EntityCard key={item.id} {...mapToCardProps(item)} />
    ))}
  </div>
)}
```

### Grille avec Loading

```tsx
{loading ? (
  <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
    {Array.from({ length: 8 }).map((_, i) => (
      <div key={i} className="bg-[var(--bg-card)] border-2 border-[var(--color-accent-200)] rounded-[var(--radius-card)] overflow-hidden animate-pulse">
        <div className="bg-[var(--color-accent-50)] p-4 border-b border-[var(--color-accent-200)]">
          <div className="flex items-center gap-3">
            <div className="w-10 h-10 rounded-lg bg-[var(--bg-tertiary)]" />
            <div className="flex-1">
              <div className="h-4 bg-[var(--bg-tertiary)] rounded w-3/4 mb-2" />
              <div className="h-3 bg-[var(--bg-tertiary)] rounded w-1/2" />
            </div>
          </div>
        </div>
        <div className="p-4">
          <div className="h-3 bg-[var(--bg-tertiary)] rounded w-full mb-2" />
          <div className="h-3 bg-[var(--bg-tertiary)] rounded w-2/3" />
        </div>
      </div>
    ))}
  </div>
) : (
  // ... actual grid
)}
```

---

## WORKFLOW DE GÉNÉRATION

### ÉTAPE 1: Identifier le Type d'Entité

| Type | Composant | Exemple |
|------|-----------|---------|
| Provider IA | `ProviderCard` | OpenAI, Anthropic |
| Template | `TemplateCard` | Email templates |
| Entité standard | `EntityCard` | Produits, Utilisateurs |
| Item avec actions | `EntityCard` + actions | Tickets, Commandes |

### ÉTAPE 2: Mapper les Props

```typescript
// Pattern de mapping
function mapEntityToCard(entity: Entity): EntityCardProps {
  return {
    avatar: {
      letter: entity.name[0].toUpperCase(),
      color: getColorForType(entity.type),
    },
    title: entity.name,
    subtitle: entity.code,
    description: entity.description,
    stats: entity.itemCount ? `${entity.itemCount} élément(s)` : undefined,
    badge: entity.hasSpecialFeature ? { icon: Star, tooltip: 'Feature spéciale' } : undefined,
    links: entity.websiteUrl ? [
      { icon: ExternalLink, label: 'Site web', href: entity.websiteUrl }
    ] : undefined,
    actions: [
      { label: 'Voir détails', onClick: () => navigate(`/${entity.id}`), variant: 'primary' }
    ],
  };
}
```

### ÉTAPE 3: Utiliser dans la Page

```tsx
// Pattern page complète
export function EntitiesPage() {
  const [entities, setEntities] = useState<Entity[]>([]);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    loadEntities();
  }, []);

  return (
    <div className="space-y-6">
      <PageHeader title="Entités" subtitle="Gérer les entités" />

      <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
        {entities.map(entity => (
          <EntityCard key={entity.id} {...mapEntityToCard(entity)} />
        ))}
      </div>
    </div>
  );
}
```

---

## QUAND UTILISER EntityCard vs Custom Cards

### Utiliser EntityCard pour:
- Listes d'entités homogènes (produits, providers, templates)
- Cards avec structure standard: avatar, titre, description, liens, actions
- Grilles de cards cliquables
- Affichage de catalogues

### NE PAS utiliser EntityCard pour:
- Dashboard stats cards (sync status, métriques)
- Cards avec états interactifs complexes (sélection, toggle inline)
- Cards avec sous-listes intégrées (ex: roles assignés avec X pour supprimer)
- Cards master/detail avec panneau de détails
- Cards avec formulaires intégrés

---

## PATTERN: CUSTOM CARDS (Status, Stats, Dashboard)

Pour les cards qui ne peuvent pas utiliser `EntityCard`, utiliser ce pattern pour garantir l'alignement des boutons en bas.

### Structure de Base avec Alignement Boutons

```tsx
// ⚠️ OBLIGATOIRE: h-full flex flex-col sur le container
<div className="h-full flex flex-col rounded-[var(--radius-card)] border border-[var(--border-color)] bg-[var(--bg-card)] overflow-hidden shadow-sm hover:shadow-md transition-shadow">

  {/* Header avec gradient */}
  <div className="px-4 py-3 bg-gradient-to-r from-[var(--color-accent-500)]/10 to-[var(--color-accent-600)]/5 border-b border-[var(--border-color)]">
    <div className="flex items-center justify-between">
      <div className="flex items-center gap-3">
        <div className="p-2 rounded-lg bg-[var(--color-accent-500)]/20">
          <Icon className="w-5 h-5 text-[var(--color-accent-600)]" />
        </div>
        <h3 className="font-semibold text-[var(--text-primary)]">
          {title}
        </h3>
      </div>
      {/* Badge de statut (optionnel) */}
      <div className="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full text-xs font-medium bg-[var(--success-bg)] text-[var(--success-text)]">
        <StatusIcon className="w-3.5 h-3.5" />
        {statusLabel}
      </div>
    </div>
  </div>

  {/* ⚠️ OBLIGATOIRE: flex-1 flex flex-col pour que le contenu prenne toute la hauteur disponible */}
  <div className="flex-1 flex flex-col p-4">

    {/* Stats Grid (optionnel) */}
    <div className="grid grid-cols-3 gap-3 mb-4">
      <StatBlock icon={Database} value={123} label="Items" />
      <StatBlock icon={Calendar} value="15 jan" label="Last sync" />
      <StatBlock icon={RefreshCw} value="14 jan" label="Delta sync" />
    </div>

    {/* Contenu variable (warnings, errors, etc.) */}
    {warning && (
      <div className="p-3 mb-4 rounded-lg bg-[var(--warning-bg)] border border-[var(--warning-border)]">
        <span className="text-sm text-[var(--warning-text)]">{warning}</span>
      </div>
    )}

    {/* ⚠️ OBLIGATOIRE: mt-auto pour pousser le bouton en bas */}
    <button className="mt-auto w-full inline-flex items-center justify-center gap-2 px-4 py-2.5 text-sm font-medium rounded-[var(--radius-button)] bg-[var(--color-accent-600)] text-white hover:bg-[var(--color-accent-700)] disabled:opacity-50 transition-colors">
      <RefreshCw className="w-4 h-4" />
      {actionLabel}
    </button>
  </div>
</div>
```

### Règles d'Alignement Boutons (CRITIQUES)

| Élément | Classe CSS | Raison |
|---------|------------|--------|
| **Card container** | `h-full flex flex-col` | Permet à la card de remplir la hauteur de la grille |
| **Content wrapper** | `flex-1 flex flex-col` | Le contenu prend l'espace disponible |
| **Action button** | `mt-auto` | Pousse le bouton tout en bas |

### Exemple Complet: Sync Status Card

```tsx
interface SyncStatusCardProps {
  resourceType: string;
  icon: LucideIcon;
  status: 'Success' | 'Failed' | 'Running' | 'Pending';
  lastSyncAt: string | null;
  itemCount: number;
  pendingConflicts: number;
  error: string | null;
  syncing: boolean;
  onSync: () => void;
}

function SyncStatusCard({
  resourceType, icon: Icon, status, lastSyncAt, itemCount,
  pendingConflicts, error, syncing, onSync
}: SyncStatusCardProps) {

  const statusConfig = getStatusConfig(status); // Returns { icon, color, bg, label }

  return (
    <div className="h-full flex flex-col rounded-[var(--radius-card)] border border-[var(--border-color)] bg-[var(--bg-card)] overflow-hidden shadow-sm hover:shadow-md transition-shadow">
      {/* Header */}
      <div className="px-4 py-3 bg-gradient-to-r from-[var(--color-accent-500)]/10 to-[var(--color-accent-600)]/5 border-b border-[var(--border-color)]">
        <div className="flex items-center justify-between">
          <div className="flex items-center gap-3">
            <div className="p-2 rounded-lg bg-[var(--color-accent-500)]/20">
              <Icon className="w-5 h-5 text-[var(--color-accent-600)]" />
            </div>
            <h3 className="font-semibold text-[var(--text-primary)]">{resourceType}</h3>
          </div>
          <div className={`inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full text-xs font-medium ${statusConfig.bg} ${statusConfig.color}`}>
            <statusConfig.icon className="w-3.5 h-3.5" />
            {statusConfig.label}
          </div>
        </div>
      </div>

      {/* Content */}
      <div className="flex-1 flex flex-col p-4">
        {/* Stats */}
        <div className="grid grid-cols-2 gap-3 mb-4">
          <div className="text-center p-3 rounded-lg bg-[var(--bg-secondary)]">
            <p className="text-xl font-bold">{itemCount}</p>
            <p className="text-xs text-[var(--text-tertiary)]">Synced</p>
          </div>
          <div className="text-center p-3 rounded-lg bg-[var(--bg-secondary)]">
            <p className="text-sm font-medium">{formatDate(lastSyncAt)}</p>
            <p className="text-xs text-[var(--text-tertiary)]">Last sync</p>
          </div>
        </div>

        {/* Warnings (variable height) */}
        {pendingConflicts > 0 && (
          <div className="p-3 mb-4 rounded-lg bg-[var(--warning-bg)] border border-[var(--warning-border)]">
            <span className="text-sm text-[var(--warning-text)]">
              {pendingConflicts} conflicts pending
            </span>
          </div>
        )}

        {/* Errors (variable height) */}
        {error && (
          <div className="p-3 mb-4 rounded-lg bg-[var(--error-bg)] border border-[var(--error-border)]">
            <p className="text-xs text-[var(--error-text)] line-clamp-2">{error}</p>
          </div>
        )}

        {/* Button - ALWAYS at bottom */}
        <button
          onClick={onSync}
          disabled={syncing}
          className="mt-auto w-full inline-flex items-center justify-center gap-2 px-4 py-2.5 text-sm font-medium rounded-[var(--radius-button)] bg-[var(--color-accent-600)] text-white hover:bg-[var(--color-accent-700)] disabled:opacity-50 transition-colors"
        >
          {syncing ? <Loader2 className="w-4 h-4 animate-spin" /> : <RefreshCw className="w-4 h-4" />}
          Sync Now
        </button>
      </div>
    </div>
  );
}
```

### Usage dans une Grille

```tsx
{/* La grille DOIT utiliser des colonnes égales */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
  {syncStatuses.map((status) => (
    <SyncStatusCard
      key={status.resourceType}
      {...status}
      onSync={() => handleSync(status.resourceType)}
    />
  ))}
</div>
```

### Pattern: Card Footer avec Métadonnées + Action

Quand une card a besoin d'afficher des métadonnées (date, statut) ET un bouton d'action en bas :

```tsx
{/* ⚠️ OBLIGATOIRE: mt-auto sur le footer pour l'aligner en bas */}
<div className="mt-auto pt-4 border-t border-[var(--border-color)]">
  <div className="flex items-center justify-between">
    {/* Métadonnées à gauche */}
    <div className="flex items-center gap-1.5 text-xs text-[var(--text-tertiary)]">
      <Calendar className="w-3.5 h-3.5" />
      {formatDate(createdAt)}
    </div>

    {/* Bouton d'action à droite */}
    <button className="inline-flex items-center gap-2 px-4 py-2 text-sm font-medium rounded-[var(--radius-button)] bg-[var(--color-accent-600)] text-white hover:bg-[var(--color-accent-700)] transition-colors shadow-sm">
      <CheckCircle className="w-4 h-4" />
      {actionLabel}
    </button>
  </div>
</div>
```

**Structure complète avec footer :**

```tsx
<div className="h-full flex flex-col rounded-[var(--radius-card)] border border-[var(--border-color)] bg-[var(--bg-card)] overflow-hidden">
  {/* Header */}
  <div className="px-4 py-3 bg-gradient-to-r from-[var(--color-accent-500)]/10 to-[var(--color-accent-600)]/5 border-b border-[var(--border-color)]">
    {/* ... */}
  </div>

  {/* Content - flex-1 flex flex-col pour occuper l'espace */}
  <div className="flex-1 flex flex-col p-4 space-y-4">
    {/* Contenu variable */}
    <p className="text-sm text-[var(--text-secondary)]">{description}</p>

    {/* Composants additionnels */}
    <SomeComponent />

    {/* ⚠️ Footer avec mt-auto - TOUJOURS en dernier dans le content */}
    <div className="mt-auto flex items-center justify-between pt-4 border-t border-[var(--border-color)]">
      <div className="flex items-center gap-1.5 text-xs text-[var(--text-tertiary)]">
        <Calendar className="w-3.5 h-3.5" />
        {date}
      </div>
      <button className="...">Action</button>
    </div>
  </div>
</div>
```

### ⚠️ Erreurs Communes à Éviter

```tsx
// ❌ MAUVAIS - Les boutons ne seront PAS alignés
<div className="rounded-lg border ...">
  <div className="p-4">
    {/* contenu */}
    <button>Action</button>  {/* Pas de mt-auto, pas d'alignement */}
  </div>
</div>

// ❌ MAUVAIS - Footer sans mt-auto
<div className="h-full flex flex-col ...">
  <div className="flex-1 flex flex-col p-4">
    {/* contenu */}
    <div className="pt-4 border-t ...">  {/* Manque mt-auto ! */}
      <button>Action</button>
    </div>
  </div>
</div>

// ✅ BON - Footer aligné en bas avec mt-auto
<div className="h-full flex flex-col rounded-lg border ...">
  <div className="flex-1 flex flex-col p-4">
    {/* contenu */}
    <div className="mt-auto pt-4 border-t ...">  {/* mt-auto présent */}
      <div className="flex items-center justify-between">
        <span>{metadata}</span>
        <button>Action</button>
      </div>
    </div>
  </div>
</div>
```

---

## RÈGLES ABSOLUES

1. **TOUJOURS** utiliser `EntityCard` pour les cards d'entités
2. **TOUJOURS** utiliser le header coloré distinct
3. **TOUJOURS** utiliser `rounded-lg` pour l'avatar (carré arrondi)
4. **TOUJOURS** utiliser les variables CSS d'accent (`--color-accent-*`)
5. **TOUJOURS** grille responsive (1→2→3→4 colonnes)
6. **TOUJOURS** gérer les états vide et loading
7. **TOUJOURS** utiliser `href` pour les liens externes (ouvre nouvel onglet)
8. **TOUJOURS** utiliser `h-full flex flex-col` + `flex-1` + `mt-auto` pour aligner les boutons en bas des custom cards
9. **JAMAIS** de `rounded-full` pour l'avatar (c'est le design DataTable/table)
10. **JAMAIS** de cards custom en divs manuels pour les entités simples
11. **JAMAIS** de grille fixe non-responsive
12. **JAMAIS** de boutons non alignés dans une grille de cards (utiliser le pattern d'alignement)

---

## COMPOSANT: DataTable

**Fichier:** `web/smartstack-web/src/components/ui/DataTable.tsx`

### Import

```typescript
import { DataTable } from '@/components/ui/DataTable';
```

### Usage

```tsx
<DataTable
  data={users}
  columns={[
    { key: 'name', label: 'Nom', sortable: true },
    { key: 'email', label: 'Email', sortable: true },
    { key: 'role', label: 'Rôle', render: (user) => <Badge>{user.role}</Badge> },
    { key: 'createdAt', label: 'Créé le', render: (user) => formatDate(user.createdAt) },
  ]}
  pagination={{ pageSize: 10, showSizeSelector: true }}
  searchable
  searchPlaceholder="Rechercher un utilisateur..."
  onRowClick={(user) => navigate(`/users/${user.id}`)}
  getRowKey={(user) => user.id}
  striped
/>
```

### Props Principales

| Prop | Type | Description |
|------|------|-------------|
| `data` | `T[]` | Données à afficher |
| `columns` | `DataTableColumn<T>[]` | Définition des colonnes |
| `pagination` | `{ pageSize, showSizeSelector? }` | Config pagination |
| `searchable` | `boolean` | Activer recherche globale |
| `onRowClick` | `(item, index) => void` | Click sur ligne |
| `selectable` | `boolean` | Activer sélection multi |
| `loading` | `boolean` | État chargement |
| `striped` | `boolean` | Lignes alternées |
| `compact` | `boolean` | Mode compact |

### Column Config

```typescript
interface DataTableColumn<T> {
  key: string;           // Clé de l'objet ou custom
  label: string;         // Label en-tête
  sortable?: boolean;    // Tri activé
  render?: (item: T) => ReactNode;  // Rendu custom
  width?: string;        // Largeur CSS
  align?: 'left' | 'center' | 'right';
  hideOnMobile?: boolean;
}
```

---

## COMPOSANT: Tooltip

**Fichier:** `web/smartstack-web/src/components/ui/Tooltip.tsx`

### Import

```typescript
import { Tooltip, type TooltipVariant, type TooltipPosition } from '@/components/ui/Tooltip';
```

### Variantes Disponibles

| Variant | Couleur | Usage |
|---------|---------|-------|
| `default` | Gris (thème) | Information neutre |
| `error` | Rouge | Permission refusée, erreur, action interdite |
| `warning` | Orange | Avertissement, action irréversible |
| `success` | Vert | Confirmation, action réussie |
| `info` | Bleu | Information contextuelle, aide |

### Positions Disponibles

| Position | Description |
|----------|-------------|
| `top` | Au-dessus de l'élément (défaut) |
| `bottom` | En dessous de l'élément |
| `left` | À gauche de l'élément |
| `right` | À droite de l'élément |

### Usage de Base

```tsx
import { Tooltip } from '@/components/ui/Tooltip';

// Tooltip par défaut (informatif)
<Tooltip content="Information contextuelle">
  <button>Hover me</button>
</Tooltip>

// Tooltip d'erreur (permission refusée)
<Tooltip
  content="Action non autorisée - permission 'execute' requise"
  variant="error"
  position="top"
>
  <button disabled>Action protégée</button>
</Tooltip>

// Tooltip de warning (action dangereuse)
<Tooltip
  content="Cette action est irréversible"
  variant="warning"
  position="bottom"
>
  <button>Supprimer</button>
</Tooltip>

// Tooltip de succès
<Tooltip content="Fichier sauvegardé" variant="success">
  <span>✓ Saved</span>
</Tooltip>

// Tooltip informatif
<Tooltip content="Cliquez pour plus de détails" variant="info">
  <HelpCircle className="w-4 h-4" />
</Tooltip>
```

### Pattern: Bouton Désactivé avec Explication

```tsx
// Pattern recommandé pour les boutons désactivés avec permission
const { hasPermission } = useAuth();
const canExecute = hasPermission('module.action.execute');

<Tooltip
  content={!canExecute ? t('errors.noPermission') : undefined}
  variant="error"
  disabled={canExecute}  // Tooltip ne s'affiche que si désactivé
>
  <button
    onClick={handleAction}
    disabled={!canExecute}
    className="... disabled:opacity-50 disabled:cursor-not-allowed"
  >
    Action
  </button>
</Tooltip>
```

### Props Complètes

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `content` | `ReactNode` | - | Contenu du tooltip (requis) |
| `children` | `ReactNode` | - | Élément déclencheur (requis) |
| `position` | `'top' \| 'bottom' \| 'left' \| 'right'` | `'top'` | Position du tooltip |
| `variant` | `'default' \| 'error' \| 'warning' \| 'success' \| 'info'` | `'default'` | Style/couleur du tooltip |
| `delay` | `number` | `200` | Délai avant affichage (ms) |
| `disabled` | `boolean` | `false` | Désactive le tooltip |
| `className` | `string` | `''` | Classes CSS additionnelles |

### Variables CSS Utilisées

Le tooltip utilise des variables CSS opaques pour garantir la lisibilité :

```css
/* Light mode */
--tooltip-error-bg: #fef2f2;
--tooltip-warning-bg: #fefce8;
--tooltip-success-bg: #f0fdf4;
--tooltip-info-bg: #eff6ff;

/* Dark mode */
--tooltip-error-bg: #3b1c1e;
--tooltip-warning-bg: #3b2f1a;
--tooltip-success-bg: #1a3329;
--tooltip-info-bg: #1a2c3b;
```

### ⚠️ Règles d'Usage

1. **TOUJOURS** utiliser `variant="error"` pour les permissions refusées
2. **TOUJOURS** utiliser `variant="warning"` pour les actions dangereuses/irréversibles
3. **TOUJOURS** désactiver le tooltip (`disabled={true}`) quand l'action est autorisée
4. **TOUJOURS** positionner le tooltip de manière à ne pas bloquer les éléments importants
5. **JAMAIS** utiliser le tooltip natif `title` HTML - utiliser ce composant à la place
6. **JAMAIS** de fonds transparents - le composant utilise des fonds opaques

---

## COMPOSANTS - RÉSUMÉ

| Composant | Fichier | Usage |
|-----------|---------|-------|
| **EntityCard** | `EntityCard.tsx` | Cards d'entités avec header coloré |
| **ProviderCard** | `EntityCard.tsx` | Preset pour providers IA |
| **TemplateCard** | `EntityCard.tsx` | Preset pour templates |
| **DataTable** | `DataTable.tsx` | Tableaux avec tri/pagination/recherche |
| **Tooltip** | `Tooltip.tsx` | Tooltips contextuels avec variantes colorées |

---

## VALIDATION

Après génération, vérifier :

```bash
# TypeScript
cd web/smartstack-web && npx tsc --noEmit

# Lint
npm run lint

# Import correct
grep -r "EntityCard" src/pages/ | grep -v "from '@/components/ui/EntityCard'"
# ↑ Doit retourner vide (tous imports corrects)
```
