@atlashub/smartstack-cli 1.4.1 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/.documentation/agents.html +916 -916
  2. package/.documentation/apex.html +1018 -1018
  3. package/.documentation/business-analyse.html +1501 -1501
  4. package/.documentation/commands.html +680 -680
  5. package/.documentation/css/styles.css +2168 -2168
  6. package/.documentation/efcore.html +2505 -2505
  7. package/.documentation/gitflow.html +2618 -2618
  8. package/.documentation/hooks.html +413 -413
  9. package/.documentation/index.html +323 -323
  10. package/.documentation/installation.html +462 -462
  11. package/.documentation/js/app.js +794 -794
  12. package/.documentation/test-web.html +513 -513
  13. package/dist/index.js +807 -277
  14. package/dist/index.js.map +1 -1
  15. package/package.json +1 -1
  16. package/templates/agents/efcore/conflicts.md +44 -17
  17. package/templates/agents/efcore/db-status.md +27 -6
  18. package/templates/agents/efcore/scan.md +43 -13
  19. package/templates/commands/ai-prompt.md +315 -315
  20. package/templates/commands/application/create.md +362 -362
  21. package/templates/commands/controller/create.md +216 -216
  22. package/templates/commands/controller.md +59 -0
  23. package/templates/commands/documentation/module.md +202 -202
  24. package/templates/commands/efcore/_env-check.md +153 -153
  25. package/templates/commands/efcore/conflicts.md +109 -192
  26. package/templates/commands/efcore/db-status.md +101 -89
  27. package/templates/commands/efcore/migration.md +23 -11
  28. package/templates/commands/efcore/scan.md +115 -119
  29. package/templates/commands/efcore.md +54 -6
  30. package/templates/commands/feature-full.md +267 -267
  31. package/templates/commands/gitflow/11-finish.md +145 -11
  32. package/templates/commands/gitflow/13-sync.md +216 -216
  33. package/templates/commands/gitflow/14-rebase.md +251 -251
  34. package/templates/commands/gitflow/2-status.md +120 -10
  35. package/templates/commands/gitflow/3-commit.md +150 -0
  36. package/templates/commands/gitflow/7-pull-request.md +134 -5
  37. package/templates/commands/gitflow/9-merge.md +142 -1
  38. package/templates/commands/implement.md +663 -663
  39. package/templates/commands/init.md +562 -0
  40. package/templates/commands/mcp-integration.md +330 -0
  41. package/templates/commands/notification.md +129 -129
  42. package/templates/commands/validate.md +233 -0
  43. package/templates/commands/workflow.md +193 -193
  44. package/templates/skills/ai-prompt/SKILL.md +778 -778
  45. package/templates/skills/application/SKILL.md +563 -563
  46. package/templates/skills/application/templates-backend.md +450 -450
  47. package/templates/skills/application/templates-frontend.md +531 -531
  48. package/templates/skills/application/templates-i18n.md +520 -520
  49. package/templates/skills/application/templates-seed.md +647 -647
  50. package/templates/skills/controller/SKILL.md +240 -240
  51. package/templates/skills/controller/postman-templates.md +614 -614
  52. package/templates/skills/controller/templates.md +1468 -1468
  53. package/templates/skills/documentation/SKILL.md +133 -133
  54. package/templates/skills/documentation/templates.md +476 -476
  55. package/templates/skills/feature-full/SKILL.md +838 -838
  56. package/templates/skills/notification/SKILL.md +555 -555
  57. package/templates/skills/ui-components/SKILL.md +870 -870
  58. package/templates/skills/workflow/SKILL.md +582 -582
@@ -1,870 +1,870 @@
1
- ---
2
- name: ui-components
3
- description: |
4
- Génère des composants UI SmartStack standardisés.
5
- Utiliser ce skill quand:
6
- - Création de page React (.tsx) dans src/pages/
7
- - Création de composant React dans src/components/
8
- - L'utilisateur demande de créer des cards, grilles, tableaux, ou Kanban
9
- - Claude détecte la création d'une liste d'entités avec affichage
10
- - L'utilisateur mentionne "card", "grille", "tableau", "liste", "kanban"
11
- - L'utilisateur demande des tooltips ou infobulles
12
- - Création d'une page avec affichage d'entités
13
- - Gestion des états désactivés avec messages explicatifs
14
- Scope: Pages, Components, Cards, Tables, Grids, Kanban boards, Tooltips
15
- ---
16
-
17
- # Skill UI Components SmartStack
18
-
19
- > **Synergie Skill/Composant:**
20
- > - **Skill** (`.claude/skills/ui-components/`) → Invocation automatique par Claude
21
- > - **Composant** (`components/ui/EntityCard.tsx`) → Source de vérité du rendu
22
- > - Templates et patterns dans ce fichier
23
-
24
- ## QUAND CE SKILL S'ACTIVE
25
-
26
- Claude invoque automatiquement ce skill quand il détecte :
27
-
28
- | Déclencheur | Exemple |
29
- |-------------|---------|
30
- | **Création page React** | Écriture de fichier dans `src/pages/**/*.tsx` |
31
- | **Création composant** | Écriture de fichier dans `src/components/**/*.tsx` |
32
- | Création de liste | "Affiche les produits en cards" |
33
- | Nouveau module avec UI | "Crée un module avec grille de cards" |
34
- | Refactoring UI | "Uniformise les cards de cette page" |
35
- | Mots-clés | "card", "grille", "tableau", "kanban", "liste" |
36
- | Tooltips/Infobulles | "Ajoute un tooltip sur ce bouton" |
37
- | États désactivés | "Désactive le bouton avec un message explicatif" |
38
- | Permissions UI | "Affiche pourquoi l'utilisateur ne peut pas cliquer" |
39
-
40
- ---
41
-
42
- ## DESIGN SYSTEM DE RÉFÉRENCE
43
-
44
- **Page de référence:** `/platform/administration/ai/settings`
45
-
46
- ### Anatomie d'une Card Standard
47
-
48
- ```
49
- ┌─────────────────────────────────────────────────────────────────────────┐
50
- │ HEADER (bg-accent-50) │
51
- │ ┌─────┐ [Badge] │
52
- │ │ O │ Titre Principal │
53
- │ └─────┘ code-slug │
54
- ├─────────────────────────────────────────────────────────────────────────┤
55
- │ BODY │
56
- │ │
57
- │ Description du contenu sur une ou plusieurs │
58
- │ lignes avec texte secondaire. │
59
- │ │
60
- │ 15 modèle(s) │
61
- │ │
62
- │ ───────────────────────────────────────────── │
63
- │ ↗ Site officiel │
64
- │ 📖 Documentation API │
65
- │ │
66
- │ ┌─────────────────────────────────────────────────────────────────────┐│
67
- │ │ Obtenir une clé API (primary) ││
68
- │ └─────────────────────────────────────────────────────────────────────┘│
69
- │ ┌─────────────────────────────────────────────────────────────────────┐│
70
- │ │ Obtenir une clé API Admin (secondary) ││
71
- │ └─────────────────────────────────────────────────────────────────────┘│
72
- └─────────────────────────────────────────────────────────────────────────┘
73
- ```
74
-
75
- ### Caractéristiques du Design
76
-
77
- | Élément | Style |
78
- |---------|-------|
79
- | **Card** | `bg-[var(--bg-card)]` avec `border-2 border-[var(--color-accent-200)]` |
80
- | **Header** | Section colorée séparée `bg-[var(--color-accent-50)]` |
81
- | **Avatar** | Carré arrondi `rounded-lg` (PAS cercle), `shadow-lg` |
82
- | **Subtitle** | Utilise `<code>` pour le code/slug |
83
- | **Badge** | Icône avec tooltip au hover, dans le header |
84
- | **Links** | Séparés par `border-t`, icône + texte |
85
- | **Actions** | Full-width, variants primary/secondary |
86
- | **Spacer** | `flex-1 min-h-4` pour push buttons en bas |
87
-
88
- ### Couleurs d'Accent
89
-
90
- ```
91
- --color-accent-50 → Header background (light)
92
- --color-accent-200 → Border color
93
- --color-accent-400 → Border hover
94
- --color-accent-500 → Button primary background
95
- --color-accent-600 → Button primary hover
96
- --color-accent-700 → Button secondary background / Badge icon
97
- --color-accent-800 → Button secondary hover / Dark mode border
98
- --color-accent-900 → Dark mode header background
99
- ```
100
-
101
- ---
102
-
103
- ## COMPOSANT OBLIGATOIRE: EntityCard
104
-
105
- **Fichier:** `web/smartstack-web/src/components/ui/EntityCard.tsx`
106
-
107
- ### RÈGLE ABSOLUE
108
-
109
- > **TOUJOURS utiliser `EntityCard` pour les cards d'entités.**
110
- > Ne JAMAIS créer de cards custom avec des divs manuels.
111
-
112
- ### Import Standard
113
-
114
- ```typescript
115
- import { EntityCard, ProviderCard, TemplateCard } from '@/components/ui/EntityCard';
116
- ```
117
-
118
- ### Usage EntityCard (Générique)
119
-
120
- ```tsx
121
- <EntityCard
122
- avatar={{ letter: 'O', color: '#10a37f' }}
123
- title="OpenAI"
124
- subtitle="openai"
125
- description="OpenAI GPT models (GPT-4, GPT-4o, GPT-3.5)"
126
- stats="15 modèle(s)"
127
- badge={{ icon: Shield, tooltip: 'API Admin supportée' }}
128
- links={[
129
- { icon: ExternalLink, label: 'Site officiel', href: 'https://openai.com' },
130
- { icon: BookOpen, label: 'Documentation API', href: 'https://platform.openai.com/docs' },
131
- ]}
132
- actions={[
133
- { label: 'Obtenir une clé API', href: 'https://...', variant: 'primary', icon: Key },
134
- { label: 'Obtenir une clé API Admin', href: 'https://...', variant: 'secondary', icon: Shield },
135
- ]}
136
- />
137
- ```
138
-
139
- ### Usage ProviderCard (AI Providers)
140
-
141
- ```tsx
142
- <ProviderCard
143
- name="OpenAI"
144
- code="openai"
145
- description="OpenAI GPT models (GPT-4, GPT-4o, GPT-3.5)"
146
- modelCount={15}
147
- color="#10a37f"
148
- websiteUrl="https://openai.com"
149
- docsUrl="https://platform.openai.com/docs"
150
- apiKeyUrl="https://platform.openai.com/api-keys"
151
- adminApiKeyUrl="https://platform.openai.com/organization/admin-keys"
152
- hasAdminKey
153
- badgeIcon={Shield}
154
- badgeTooltip="API Admin supportée"
155
- />
156
- ```
157
-
158
- ### Usage TemplateCard (Templates)
159
-
160
- ```tsx
161
- <TemplateCard
162
- name="Welcome Email"
163
- code="welcome"
164
- category="Transactional"
165
- isActive
166
- isSystem
167
- icon={Mail}
168
- iconColor="var(--color-accent-500)"
169
- translationsCount={3}
170
- onClick={() => navigate('/...')}
171
- onEdit={() => navigate('/edit')}
172
- onDelete={() => handleDelete()}
173
- labels={{
174
- activeLabel: t('emailTemplates.active'),
175
- inactiveLabel: t('emailTemplates.inactive'),
176
- systemLabel: 'System',
177
- editLabel: t('emailTemplates.edit'),
178
- deleteLabel: t('emailTemplates.delete'),
179
- }}
180
- />
181
- ```
182
-
183
- ### TemplateCard Props
184
-
185
- | Prop | Type | Description |
186
- |------|------|-------------|
187
- | `name` | `string` | Nom du template (requis) |
188
- | `code` | `string` | Code/slug du template (requis) |
189
- | `category` | `string` | Catégorie affichée en tag |
190
- | `isActive` | `boolean` | État actif/inactif (badge dans header) |
191
- | `isSystem` | `boolean` | Template système (non supprimable) |
192
- | `icon` | `ElementType` | Icône Lucide pour l'avatar |
193
- | `iconColor` | `string` | Couleur de l'avatar (CSS) |
194
- | `translationsCount` | `number` | Nombre de traductions |
195
- | `onClick` | `() => void` | Click sur la card |
196
- | `onEdit` | `() => void` | Bouton éditer |
197
- | `onDelete` | `() => void` | Bouton supprimer (masqué si isSystem) |
198
- | `labels` | `object` | Labels i18n pour tous les textes |
199
-
200
- ---
201
-
202
- ## PROPS RÉFÉRENCE
203
-
204
- ### EntityCard Props
205
-
206
- | Prop | Type | Description |
207
- |------|------|-------------|
208
- | `avatar` | `{ letter, color, imageUrl? }` | Avatar carré arrondi |
209
- | `title` | `string` | Titre principal (requis) |
210
- | `subtitle` | `string` | Code/slug en gris (affiché en `<code>`) |
211
- | `description` | `string \| ReactNode` | Description (line-clamp-2) |
212
- | `stats` | `string \| ReactNode` | Statistique (nombre en bold) |
213
- | `badge` | `{ icon?, tooltip?, color? }` | Badge dans le header |
214
- | `links` | `Array<{ icon, label, href?, onClick? }>` | Liens avec icônes |
215
- | `actions` | `Array<{ label, href?, onClick?, variant, icon?, disabled?, loading? }>` | Boutons d'action |
216
- | `tags` | `Array<{ label, variant, onClick? }>` | Tags/catégories |
217
- | `onClick` | `() => void` | Click sur la card |
218
- | `customHeader` | `ReactNode` | Remplace le header par défaut |
219
- | `customBody` | `ReactNode` | Remplace le body par défaut |
220
- | `customFooter` | `ReactNode` | Remplace le footer par défaut |
221
-
222
- ### Action Props
223
-
224
- | Prop | Type | Description |
225
- |------|------|-------------|
226
- | `label` | `string` | Texte du bouton |
227
- | `href` | `string` | URL (rend un `<a>` au lieu de `<button>`) |
228
- | `onClick` | `() => void` | Handler de click |
229
- | `variant` | `'primary' \| 'secondary' \| 'ghost'` | Style du bouton |
230
- | `icon` | `ElementType` | Icône Lucide |
231
- | `disabled` | `boolean` | État désactivé |
232
- | `loading` | `boolean` | État de chargement |
233
-
234
- ### Action Variants
235
-
236
- | Variant | Style |
237
- |---------|-------|
238
- | `primary` | `bg-accent-500 hover:bg-accent-600 text-white` |
239
- | `secondary` | `bg-accent-700 hover:bg-accent-800 text-white` |
240
- | `ghost` | `border border-border-color bg-transparent` |
241
-
242
- ---
243
-
244
- ## PATTERNS DE GRILLE
245
-
246
- ### Grille Responsive Standard
247
-
248
- ```tsx
249
- <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
250
- {items.map(item => (
251
- <EntityCard key={item.id} {...mapToCardProps(item)} />
252
- ))}
253
- </div>
254
- ```
255
-
256
- ### Grille avec État Vide
257
-
258
- ```tsx
259
- {items.length === 0 ? (
260
- <div className="col-span-full text-center py-12">
261
- <Package className="w-12 h-12 mx-auto mb-4 opacity-50" />
262
- <p className="text-[var(--text-secondary)]">Aucun élément trouvé</p>
263
- </div>
264
- ) : (
265
- <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
266
- {items.map(item => (
267
- <EntityCard key={item.id} {...mapToCardProps(item)} />
268
- ))}
269
- </div>
270
- )}
271
- ```
272
-
273
- ### Grille avec Loading
274
-
275
- ```tsx
276
- {loading ? (
277
- <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
278
- {Array.from({ length: 8 }).map((_, i) => (
279
- <div key={i} className="bg-[var(--bg-card)] border-2 border-[var(--color-accent-200)] rounded-[var(--radius-card)] overflow-hidden animate-pulse">
280
- <div className="bg-[var(--color-accent-50)] p-4 border-b border-[var(--color-accent-200)]">
281
- <div className="flex items-center gap-3">
282
- <div className="w-10 h-10 rounded-lg bg-[var(--bg-tertiary)]" />
283
- <div className="flex-1">
284
- <div className="h-4 bg-[var(--bg-tertiary)] rounded w-3/4 mb-2" />
285
- <div className="h-3 bg-[var(--bg-tertiary)] rounded w-1/2" />
286
- </div>
287
- </div>
288
- </div>
289
- <div className="p-4">
290
- <div className="h-3 bg-[var(--bg-tertiary)] rounded w-full mb-2" />
291
- <div className="h-3 bg-[var(--bg-tertiary)] rounded w-2/3" />
292
- </div>
293
- </div>
294
- ))}
295
- </div>
296
- ) : (
297
- // ... actual grid
298
- )}
299
- ```
300
-
301
- ---
302
-
303
- ## WORKFLOW DE GÉNÉRATION
304
-
305
- ### ÉTAPE 1: Identifier le Type d'Entité
306
-
307
- | Type | Composant | Exemple |
308
- |------|-----------|---------|
309
- | Provider IA | `ProviderCard` | OpenAI, Anthropic |
310
- | Template | `TemplateCard` | Email templates |
311
- | Entité standard | `EntityCard` | Produits, Utilisateurs |
312
- | Item avec actions | `EntityCard` + actions | Tickets, Commandes |
313
-
314
- ### ÉTAPE 2: Mapper les Props
315
-
316
- ```typescript
317
- // Pattern de mapping
318
- function mapEntityToCard(entity: Entity): EntityCardProps {
319
- return {
320
- avatar: {
321
- letter: entity.name[0].toUpperCase(),
322
- color: getColorForType(entity.type),
323
- },
324
- title: entity.name,
325
- subtitle: entity.code,
326
- description: entity.description,
327
- stats: entity.itemCount ? `${entity.itemCount} élément(s)` : undefined,
328
- badge: entity.hasSpecialFeature ? { icon: Star, tooltip: 'Feature spéciale' } : undefined,
329
- links: entity.websiteUrl ? [
330
- { icon: ExternalLink, label: 'Site web', href: entity.websiteUrl }
331
- ] : undefined,
332
- actions: [
333
- { label: 'Voir détails', onClick: () => navigate(`/${entity.id}`), variant: 'primary' }
334
- ],
335
- };
336
- }
337
- ```
338
-
339
- ### ÉTAPE 3: Utiliser dans la Page
340
-
341
- ```tsx
342
- // Pattern page complète
343
- export function EntitiesPage() {
344
- const [entities, setEntities] = useState<Entity[]>([]);
345
- const [loading, setLoading] = useState(true);
346
-
347
- useEffect(() => {
348
- loadEntities();
349
- }, []);
350
-
351
- return (
352
- <div className="space-y-6">
353
- <PageHeader title="Entités" subtitle="Gérer les entités" />
354
-
355
- <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
356
- {entities.map(entity => (
357
- <EntityCard key={entity.id} {...mapEntityToCard(entity)} />
358
- ))}
359
- </div>
360
- </div>
361
- );
362
- }
363
- ```
364
-
365
- ---
366
-
367
- ## QUAND UTILISER EntityCard vs Custom Cards
368
-
369
- ### Utiliser EntityCard pour:
370
- - Listes d'entités homogènes (produits, providers, templates)
371
- - Cards avec structure standard: avatar, titre, description, liens, actions
372
- - Grilles de cards cliquables
373
- - Affichage de catalogues
374
-
375
- ### NE PAS utiliser EntityCard pour:
376
- - Dashboard stats cards (sync status, métriques)
377
- - Cards avec états interactifs complexes (sélection, toggle inline)
378
- - Cards avec sous-listes intégrées (ex: roles assignés avec X pour supprimer)
379
- - Cards master/detail avec panneau de détails
380
- - Cards avec formulaires intégrés
381
-
382
- ---
383
-
384
- ## PATTERN: CUSTOM CARDS (Status, Stats, Dashboard)
385
-
386
- Pour les cards qui ne peuvent pas utiliser `EntityCard`, utiliser ce pattern pour garantir l'alignement des boutons en bas.
387
-
388
- ### Structure de Base avec Alignement Boutons
389
-
390
- ```tsx
391
- // ⚠️ OBLIGATOIRE: h-full flex flex-col sur le container
392
- <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">
393
-
394
- {/* Header avec gradient */}
395
- <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)]">
396
- <div className="flex items-center justify-between">
397
- <div className="flex items-center gap-3">
398
- <div className="p-2 rounded-lg bg-[var(--color-accent-500)]/20">
399
- <Icon className="w-5 h-5 text-[var(--color-accent-600)]" />
400
- </div>
401
- <h3 className="font-semibold text-[var(--text-primary)]">
402
- {title}
403
- </h3>
404
- </div>
405
- {/* Badge de statut (optionnel) */}
406
- <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)]">
407
- <StatusIcon className="w-3.5 h-3.5" />
408
- {statusLabel}
409
- </div>
410
- </div>
411
- </div>
412
-
413
- {/* ⚠️ OBLIGATOIRE: flex-1 flex flex-col pour que le contenu prenne toute la hauteur disponible */}
414
- <div className="flex-1 flex flex-col p-4">
415
-
416
- {/* Stats Grid (optionnel) */}
417
- <div className="grid grid-cols-3 gap-3 mb-4">
418
- <StatBlock icon={Database} value={123} label="Items" />
419
- <StatBlock icon={Calendar} value="15 jan" label="Last sync" />
420
- <StatBlock icon={RefreshCw} value="14 jan" label="Delta sync" />
421
- </div>
422
-
423
- {/* Contenu variable (warnings, errors, etc.) */}
424
- {warning && (
425
- <div className="p-3 mb-4 rounded-lg bg-[var(--warning-bg)] border border-[var(--warning-border)]">
426
- <span className="text-sm text-[var(--warning-text)]">{warning}</span>
427
- </div>
428
- )}
429
-
430
- {/* ⚠️ OBLIGATOIRE: mt-auto pour pousser le bouton en bas */}
431
- <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">
432
- <RefreshCw className="w-4 h-4" />
433
- {actionLabel}
434
- </button>
435
- </div>
436
- </div>
437
- ```
438
-
439
- ### Règles d'Alignement Boutons (CRITIQUES)
440
-
441
- | Élément | Classe CSS | Raison |
442
- |---------|------------|--------|
443
- | **Card container** | `h-full flex flex-col` | Permet à la card de remplir la hauteur de la grille |
444
- | **Content wrapper** | `flex-1 flex flex-col` | Le contenu prend l'espace disponible |
445
- | **Action button** | `mt-auto` | Pousse le bouton tout en bas |
446
-
447
- ### Exemple Complet: Sync Status Card
448
-
449
- ```tsx
450
- interface SyncStatusCardProps {
451
- resourceType: string;
452
- icon: LucideIcon;
453
- status: 'Success' | 'Failed' | 'Running' | 'Pending';
454
- lastSyncAt: string | null;
455
- itemCount: number;
456
- pendingConflicts: number;
457
- error: string | null;
458
- syncing: boolean;
459
- onSync: () => void;
460
- }
461
-
462
- function SyncStatusCard({
463
- resourceType, icon: Icon, status, lastSyncAt, itemCount,
464
- pendingConflicts, error, syncing, onSync
465
- }: SyncStatusCardProps) {
466
-
467
- const statusConfig = getStatusConfig(status); // Returns { icon, color, bg, label }
468
-
469
- return (
470
- <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">
471
- {/* Header */}
472
- <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)]">
473
- <div className="flex items-center justify-between">
474
- <div className="flex items-center gap-3">
475
- <div className="p-2 rounded-lg bg-[var(--color-accent-500)]/20">
476
- <Icon className="w-5 h-5 text-[var(--color-accent-600)]" />
477
- </div>
478
- <h3 className="font-semibold text-[var(--text-primary)]">{resourceType}</h3>
479
- </div>
480
- <div className={`inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full text-xs font-medium ${statusConfig.bg} ${statusConfig.color}`}>
481
- <statusConfig.icon className="w-3.5 h-3.5" />
482
- {statusConfig.label}
483
- </div>
484
- </div>
485
- </div>
486
-
487
- {/* Content */}
488
- <div className="flex-1 flex flex-col p-4">
489
- {/* Stats */}
490
- <div className="grid grid-cols-2 gap-3 mb-4">
491
- <div className="text-center p-3 rounded-lg bg-[var(--bg-secondary)]">
492
- <p className="text-xl font-bold">{itemCount}</p>
493
- <p className="text-xs text-[var(--text-tertiary)]">Synced</p>
494
- </div>
495
- <div className="text-center p-3 rounded-lg bg-[var(--bg-secondary)]">
496
- <p className="text-sm font-medium">{formatDate(lastSyncAt)}</p>
497
- <p className="text-xs text-[var(--text-tertiary)]">Last sync</p>
498
- </div>
499
- </div>
500
-
501
- {/* Warnings (variable height) */}
502
- {pendingConflicts > 0 && (
503
- <div className="p-3 mb-4 rounded-lg bg-[var(--warning-bg)] border border-[var(--warning-border)]">
504
- <span className="text-sm text-[var(--warning-text)]">
505
- {pendingConflicts} conflicts pending
506
- </span>
507
- </div>
508
- )}
509
-
510
- {/* Errors (variable height) */}
511
- {error && (
512
- <div className="p-3 mb-4 rounded-lg bg-[var(--error-bg)] border border-[var(--error-border)]">
513
- <p className="text-xs text-[var(--error-text)] line-clamp-2">{error}</p>
514
- </div>
515
- )}
516
-
517
- {/* Button - ALWAYS at bottom */}
518
- <button
519
- onClick={onSync}
520
- disabled={syncing}
521
- 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"
522
- >
523
- {syncing ? <Loader2 className="w-4 h-4 animate-spin" /> : <RefreshCw className="w-4 h-4" />}
524
- Sync Now
525
- </button>
526
- </div>
527
- </div>
528
- );
529
- }
530
- ```
531
-
532
- ### Usage dans une Grille
533
-
534
- ```tsx
535
- {/* La grille DOIT utiliser des colonnes égales */}
536
- <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
537
- {syncStatuses.map((status) => (
538
- <SyncStatusCard
539
- key={status.resourceType}
540
- {...status}
541
- onSync={() => handleSync(status.resourceType)}
542
- />
543
- ))}
544
- </div>
545
- ```
546
-
547
- ### Pattern: Card Footer avec Métadonnées + Action
548
-
549
- Quand une card a besoin d'afficher des métadonnées (date, statut) ET un bouton d'action en bas :
550
-
551
- ```tsx
552
- {/* ⚠️ OBLIGATOIRE: mt-auto sur le footer pour l'aligner en bas */}
553
- <div className="mt-auto pt-4 border-t border-[var(--border-color)]">
554
- <div className="flex items-center justify-between">
555
- {/* Métadonnées à gauche */}
556
- <div className="flex items-center gap-1.5 text-xs text-[var(--text-tertiary)]">
557
- <Calendar className="w-3.5 h-3.5" />
558
- {formatDate(createdAt)}
559
- </div>
560
-
561
- {/* Bouton d'action à droite */}
562
- <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">
563
- <CheckCircle className="w-4 h-4" />
564
- {actionLabel}
565
- </button>
566
- </div>
567
- </div>
568
- ```
569
-
570
- **Structure complète avec footer :**
571
-
572
- ```tsx
573
- <div className="h-full flex flex-col rounded-[var(--radius-card)] border border-[var(--border-color)] bg-[var(--bg-card)] overflow-hidden">
574
- {/* Header */}
575
- <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)]">
576
- {/* ... */}
577
- </div>
578
-
579
- {/* Content - flex-1 flex flex-col pour occuper l'espace */}
580
- <div className="flex-1 flex flex-col p-4 space-y-4">
581
- {/* Contenu variable */}
582
- <p className="text-sm text-[var(--text-secondary)]">{description}</p>
583
-
584
- {/* Composants additionnels */}
585
- <SomeComponent />
586
-
587
- {/* ⚠️ Footer avec mt-auto - TOUJOURS en dernier dans le content */}
588
- <div className="mt-auto flex items-center justify-between pt-4 border-t border-[var(--border-color)]">
589
- <div className="flex items-center gap-1.5 text-xs text-[var(--text-tertiary)]">
590
- <Calendar className="w-3.5 h-3.5" />
591
- {date}
592
- </div>
593
- <button className="...">Action</button>
594
- </div>
595
- </div>
596
- </div>
597
- ```
598
-
599
- ### ⚠️ Erreurs Communes à Éviter
600
-
601
- ```tsx
602
- // ❌ MAUVAIS - Les boutons ne seront PAS alignés
603
- <div className="rounded-lg border ...">
604
- <div className="p-4">
605
- {/* contenu */}
606
- <button>Action</button> {/* Pas de mt-auto, pas d'alignement */}
607
- </div>
608
- </div>
609
-
610
- // ❌ MAUVAIS - Footer sans mt-auto
611
- <div className="h-full flex flex-col ...">
612
- <div className="flex-1 flex flex-col p-4">
613
- {/* contenu */}
614
- <div className="pt-4 border-t ..."> {/* Manque mt-auto ! */}
615
- <button>Action</button>
616
- </div>
617
- </div>
618
- </div>
619
-
620
- // ✅ BON - Footer aligné en bas avec mt-auto
621
- <div className="h-full flex flex-col rounded-lg border ...">
622
- <div className="flex-1 flex flex-col p-4">
623
- {/* contenu */}
624
- <div className="mt-auto pt-4 border-t ..."> {/* mt-auto présent */}
625
- <div className="flex items-center justify-between">
626
- <span>{metadata}</span>
627
- <button>Action</button>
628
- </div>
629
- </div>
630
- </div>
631
- </div>
632
- ```
633
-
634
- ---
635
-
636
- ## RÈGLES ABSOLUES
637
-
638
- 1. **TOUJOURS** utiliser `EntityCard` pour les cards d'entités
639
- 2. **TOUJOURS** utiliser le header coloré distinct
640
- 3. **TOUJOURS** utiliser `rounded-lg` pour l'avatar (carré arrondi)
641
- 4. **TOUJOURS** utiliser les variables CSS d'accent (`--color-accent-*`)
642
- 5. **TOUJOURS** grille responsive (1→2→3→4 colonnes)
643
- 6. **TOUJOURS** gérer les états vide et loading
644
- 7. **TOUJOURS** utiliser `href` pour les liens externes (ouvre nouvel onglet)
645
- 8. **TOUJOURS** utiliser `h-full flex flex-col` + `flex-1` + `mt-auto` pour aligner les boutons en bas des custom cards
646
- 9. **JAMAIS** de `rounded-full` pour l'avatar (c'est le design DataTable/table)
647
- 10. **JAMAIS** de cards custom en divs manuels pour les entités simples
648
- 11. **JAMAIS** de grille fixe non-responsive
649
- 12. **JAMAIS** de boutons non alignés dans une grille de cards (utiliser le pattern d'alignement)
650
-
651
- ---
652
-
653
- ## COMPOSANT: DataTable
654
-
655
- **Fichier:** `web/smartstack-web/src/components/ui/DataTable.tsx`
656
-
657
- ### Import
658
-
659
- ```typescript
660
- import { DataTable } from '@/components/ui/DataTable';
661
- ```
662
-
663
- ### Usage
664
-
665
- ```tsx
666
- <DataTable
667
- data={users}
668
- columns={[
669
- { key: 'name', label: 'Nom', sortable: true },
670
- { key: 'email', label: 'Email', sortable: true },
671
- { key: 'role', label: 'Rôle', render: (user) => <Badge>{user.role}</Badge> },
672
- { key: 'createdAt', label: 'Créé le', render: (user) => formatDate(user.createdAt) },
673
- ]}
674
- pagination={{ pageSize: 10, showSizeSelector: true }}
675
- searchable
676
- searchPlaceholder="Rechercher un utilisateur..."
677
- onRowClick={(user) => navigate(`/users/${user.id}`)}
678
- getRowKey={(user) => user.id}
679
- striped
680
- />
681
- ```
682
-
683
- ### Props Principales
684
-
685
- | Prop | Type | Description |
686
- |------|------|-------------|
687
- | `data` | `T[]` | Données à afficher |
688
- | `columns` | `DataTableColumn<T>[]` | Définition des colonnes |
689
- | `pagination` | `{ pageSize, showSizeSelector? }` | Config pagination |
690
- | `searchable` | `boolean` | Activer recherche globale |
691
- | `onRowClick` | `(item, index) => void` | Click sur ligne |
692
- | `selectable` | `boolean` | Activer sélection multi |
693
- | `loading` | `boolean` | État chargement |
694
- | `striped` | `boolean` | Lignes alternées |
695
- | `compact` | `boolean` | Mode compact |
696
-
697
- ### Column Config
698
-
699
- ```typescript
700
- interface DataTableColumn<T> {
701
- key: string; // Clé de l'objet ou custom
702
- label: string; // Label en-tête
703
- sortable?: boolean; // Tri activé
704
- render?: (item: T) => ReactNode; // Rendu custom
705
- width?: string; // Largeur CSS
706
- align?: 'left' | 'center' | 'right';
707
- hideOnMobile?: boolean;
708
- }
709
- ```
710
-
711
- ---
712
-
713
- ## COMPOSANT: Tooltip
714
-
715
- **Fichier:** `web/smartstack-web/src/components/ui/Tooltip.tsx`
716
-
717
- ### Import
718
-
719
- ```typescript
720
- import { Tooltip, type TooltipVariant, type TooltipPosition } from '@/components/ui/Tooltip';
721
- ```
722
-
723
- ### Variantes Disponibles
724
-
725
- | Variant | Couleur | Usage |
726
- |---------|---------|-------|
727
- | `default` | Gris (thème) | Information neutre |
728
- | `error` | Rouge | Permission refusée, erreur, action interdite |
729
- | `warning` | Orange | Avertissement, action irréversible |
730
- | `success` | Vert | Confirmation, action réussie |
731
- | `info` | Bleu | Information contextuelle, aide |
732
-
733
- ### Positions Disponibles
734
-
735
- | Position | Description |
736
- |----------|-------------|
737
- | `top` | Au-dessus de l'élément (défaut) |
738
- | `bottom` | En dessous de l'élément |
739
- | `left` | À gauche de l'élément |
740
- | `right` | À droite de l'élément |
741
-
742
- ### Usage de Base
743
-
744
- ```tsx
745
- import { Tooltip } from '@/components/ui/Tooltip';
746
-
747
- // Tooltip par défaut (informatif)
748
- <Tooltip content="Information contextuelle">
749
- <button>Hover me</button>
750
- </Tooltip>
751
-
752
- // Tooltip d'erreur (permission refusée)
753
- <Tooltip
754
- content="Action non autorisée - permission 'execute' requise"
755
- variant="error"
756
- position="top"
757
- >
758
- <button disabled>Action protégée</button>
759
- </Tooltip>
760
-
761
- // Tooltip de warning (action dangereuse)
762
- <Tooltip
763
- content="Cette action est irréversible"
764
- variant="warning"
765
- position="bottom"
766
- >
767
- <button>Supprimer</button>
768
- </Tooltip>
769
-
770
- // Tooltip de succès
771
- <Tooltip content="Fichier sauvegardé" variant="success">
772
- <span>✓ Saved</span>
773
- </Tooltip>
774
-
775
- // Tooltip informatif
776
- <Tooltip content="Cliquez pour plus de détails" variant="info">
777
- <HelpCircle className="w-4 h-4" />
778
- </Tooltip>
779
- ```
780
-
781
- ### Pattern: Bouton Désactivé avec Explication
782
-
783
- ```tsx
784
- // Pattern recommandé pour les boutons désactivés avec permission
785
- const { hasPermission } = useAuth();
786
- const canExecute = hasPermission('module.action.execute');
787
-
788
- <Tooltip
789
- content={!canExecute ? t('errors.noPermission') : undefined}
790
- variant="error"
791
- disabled={canExecute} // Tooltip ne s'affiche que si désactivé
792
- >
793
- <button
794
- onClick={handleAction}
795
- disabled={!canExecute}
796
- className="... disabled:opacity-50 disabled:cursor-not-allowed"
797
- >
798
- Action
799
- </button>
800
- </Tooltip>
801
- ```
802
-
803
- ### Props Complètes
804
-
805
- | Prop | Type | Default | Description |
806
- |------|------|---------|-------------|
807
- | `content` | `ReactNode` | - | Contenu du tooltip (requis) |
808
- | `children` | `ReactNode` | - | Élément déclencheur (requis) |
809
- | `position` | `'top' \| 'bottom' \| 'left' \| 'right'` | `'top'` | Position du tooltip |
810
- | `variant` | `'default' \| 'error' \| 'warning' \| 'success' \| 'info'` | `'default'` | Style/couleur du tooltip |
811
- | `delay` | `number` | `200` | Délai avant affichage (ms) |
812
- | `disabled` | `boolean` | `false` | Désactive le tooltip |
813
- | `className` | `string` | `''` | Classes CSS additionnelles |
814
-
815
- ### Variables CSS Utilisées
816
-
817
- Le tooltip utilise des variables CSS opaques pour garantir la lisibilité :
818
-
819
- ```css
820
- /* Light mode */
821
- --tooltip-error-bg: #fef2f2;
822
- --tooltip-warning-bg: #fefce8;
823
- --tooltip-success-bg: #f0fdf4;
824
- --tooltip-info-bg: #eff6ff;
825
-
826
- /* Dark mode */
827
- --tooltip-error-bg: #3b1c1e;
828
- --tooltip-warning-bg: #3b2f1a;
829
- --tooltip-success-bg: #1a3329;
830
- --tooltip-info-bg: #1a2c3b;
831
- ```
832
-
833
- ### ⚠️ Règles d'Usage
834
-
835
- 1. **TOUJOURS** utiliser `variant="error"` pour les permissions refusées
836
- 2. **TOUJOURS** utiliser `variant="warning"` pour les actions dangereuses/irréversibles
837
- 3. **TOUJOURS** désactiver le tooltip (`disabled={true}`) quand l'action est autorisée
838
- 4. **TOUJOURS** positionner le tooltip de manière à ne pas bloquer les éléments importants
839
- 5. **JAMAIS** utiliser le tooltip natif `title` HTML - utiliser ce composant à la place
840
- 6. **JAMAIS** de fonds transparents - le composant utilise des fonds opaques
841
-
842
- ---
843
-
844
- ## COMPOSANTS - RÉSUMÉ
845
-
846
- | Composant | Fichier | Usage |
847
- |-----------|---------|-------|
848
- | **EntityCard** | `EntityCard.tsx` | Cards d'entités avec header coloré |
849
- | **ProviderCard** | `EntityCard.tsx` | Preset pour providers IA |
850
- | **TemplateCard** | `EntityCard.tsx` | Preset pour templates |
851
- | **DataTable** | `DataTable.tsx` | Tableaux avec tri/pagination/recherche |
852
- | **Tooltip** | `Tooltip.tsx` | Tooltips contextuels avec variantes colorées |
853
-
854
- ---
855
-
856
- ## VALIDATION
857
-
858
- Après génération, vérifier :
859
-
860
- ```bash
861
- # TypeScript
862
- cd web/smartstack-web && npx tsc --noEmit
863
-
864
- # Lint
865
- npm run lint
866
-
867
- # Import correct
868
- grep -r "EntityCard" src/pages/ | grep -v "from '@/components/ui/EntityCard'"
869
- # ↑ Doit retourner vide (tous imports corrects)
870
- ```
1
+ ---
2
+ name: ui-components
3
+ description: |
4
+ Génère des composants UI SmartStack standardisés.
5
+ Utiliser ce skill quand:
6
+ - Création de page React (.tsx) dans src/pages/
7
+ - Création de composant React dans src/components/
8
+ - L'utilisateur demande de créer des cards, grilles, tableaux, ou Kanban
9
+ - Claude détecte la création d'une liste d'entités avec affichage
10
+ - L'utilisateur mentionne "card", "grille", "tableau", "liste", "kanban"
11
+ - L'utilisateur demande des tooltips ou infobulles
12
+ - Création d'une page avec affichage d'entités
13
+ - Gestion des états désactivés avec messages explicatifs
14
+ Scope: Pages, Components, Cards, Tables, Grids, Kanban boards, Tooltips
15
+ ---
16
+
17
+ # Skill UI Components SmartStack
18
+
19
+ > **Synergie Skill/Composant:**
20
+ > - **Skill** (`.claude/skills/ui-components/`) → Invocation automatique par Claude
21
+ > - **Composant** (`components/ui/EntityCard.tsx`) → Source de vérité du rendu
22
+ > - Templates et patterns dans ce fichier
23
+
24
+ ## QUAND CE SKILL S'ACTIVE
25
+
26
+ Claude invoque automatiquement ce skill quand il détecte :
27
+
28
+ | Déclencheur | Exemple |
29
+ |-------------|---------|
30
+ | **Création page React** | Écriture de fichier dans `src/pages/**/*.tsx` |
31
+ | **Création composant** | Écriture de fichier dans `src/components/**/*.tsx` |
32
+ | Création de liste | "Affiche les produits en cards" |
33
+ | Nouveau module avec UI | "Crée un module avec grille de cards" |
34
+ | Refactoring UI | "Uniformise les cards de cette page" |
35
+ | Mots-clés | "card", "grille", "tableau", "kanban", "liste" |
36
+ | Tooltips/Infobulles | "Ajoute un tooltip sur ce bouton" |
37
+ | États désactivés | "Désactive le bouton avec un message explicatif" |
38
+ | Permissions UI | "Affiche pourquoi l'utilisateur ne peut pas cliquer" |
39
+
40
+ ---
41
+
42
+ ## DESIGN SYSTEM DE RÉFÉRENCE
43
+
44
+ **Page de référence:** `/platform/administration/ai/settings`
45
+
46
+ ### Anatomie d'une Card Standard
47
+
48
+ ```
49
+ ┌─────────────────────────────────────────────────────────────────────────┐
50
+ │ HEADER (bg-accent-50) │
51
+ │ ┌─────┐ [Badge] │
52
+ │ │ O │ Titre Principal │
53
+ │ └─────┘ code-slug │
54
+ ├─────────────────────────────────────────────────────────────────────────┤
55
+ │ BODY │
56
+ │ │
57
+ │ Description du contenu sur une ou plusieurs │
58
+ │ lignes avec texte secondaire. │
59
+ │ │
60
+ │ 15 modèle(s) │
61
+ │ │
62
+ │ ───────────────────────────────────────────── │
63
+ │ ↗ Site officiel │
64
+ │ 📖 Documentation API │
65
+ │ │
66
+ │ ┌─────────────────────────────────────────────────────────────────────┐│
67
+ │ │ Obtenir une clé API (primary) ││
68
+ │ └─────────────────────────────────────────────────────────────────────┘│
69
+ │ ┌─────────────────────────────────────────────────────────────────────┐│
70
+ │ │ Obtenir une clé API Admin (secondary) ││
71
+ │ └─────────────────────────────────────────────────────────────────────┘│
72
+ └─────────────────────────────────────────────────────────────────────────┘
73
+ ```
74
+
75
+ ### Caractéristiques du Design
76
+
77
+ | Élément | Style |
78
+ |---------|-------|
79
+ | **Card** | `bg-[var(--bg-card)]` avec `border-2 border-[var(--color-accent-200)]` |
80
+ | **Header** | Section colorée séparée `bg-[var(--color-accent-50)]` |
81
+ | **Avatar** | Carré arrondi `rounded-lg` (PAS cercle), `shadow-lg` |
82
+ | **Subtitle** | Utilise `<code>` pour le code/slug |
83
+ | **Badge** | Icône avec tooltip au hover, dans le header |
84
+ | **Links** | Séparés par `border-t`, icône + texte |
85
+ | **Actions** | Full-width, variants primary/secondary |
86
+ | **Spacer** | `flex-1 min-h-4` pour push buttons en bas |
87
+
88
+ ### Couleurs d'Accent
89
+
90
+ ```
91
+ --color-accent-50 → Header background (light)
92
+ --color-accent-200 → Border color
93
+ --color-accent-400 → Border hover
94
+ --color-accent-500 → Button primary background
95
+ --color-accent-600 → Button primary hover
96
+ --color-accent-700 → Button secondary background / Badge icon
97
+ --color-accent-800 → Button secondary hover / Dark mode border
98
+ --color-accent-900 → Dark mode header background
99
+ ```
100
+
101
+ ---
102
+
103
+ ## COMPOSANT OBLIGATOIRE: EntityCard
104
+
105
+ **Fichier:** `web/smartstack-web/src/components/ui/EntityCard.tsx`
106
+
107
+ ### RÈGLE ABSOLUE
108
+
109
+ > **TOUJOURS utiliser `EntityCard` pour les cards d'entités.**
110
+ > Ne JAMAIS créer de cards custom avec des divs manuels.
111
+
112
+ ### Import Standard
113
+
114
+ ```typescript
115
+ import { EntityCard, ProviderCard, TemplateCard } from '@/components/ui/EntityCard';
116
+ ```
117
+
118
+ ### Usage EntityCard (Générique)
119
+
120
+ ```tsx
121
+ <EntityCard
122
+ avatar={{ letter: 'O', color: '#10a37f' }}
123
+ title="OpenAI"
124
+ subtitle="openai"
125
+ description="OpenAI GPT models (GPT-4, GPT-4o, GPT-3.5)"
126
+ stats="15 modèle(s)"
127
+ badge={{ icon: Shield, tooltip: 'API Admin supportée' }}
128
+ links={[
129
+ { icon: ExternalLink, label: 'Site officiel', href: 'https://openai.com' },
130
+ { icon: BookOpen, label: 'Documentation API', href: 'https://platform.openai.com/docs' },
131
+ ]}
132
+ actions={[
133
+ { label: 'Obtenir une clé API', href: 'https://...', variant: 'primary', icon: Key },
134
+ { label: 'Obtenir une clé API Admin', href: 'https://...', variant: 'secondary', icon: Shield },
135
+ ]}
136
+ />
137
+ ```
138
+
139
+ ### Usage ProviderCard (AI Providers)
140
+
141
+ ```tsx
142
+ <ProviderCard
143
+ name="OpenAI"
144
+ code="openai"
145
+ description="OpenAI GPT models (GPT-4, GPT-4o, GPT-3.5)"
146
+ modelCount={15}
147
+ color="#10a37f"
148
+ websiteUrl="https://openai.com"
149
+ docsUrl="https://platform.openai.com/docs"
150
+ apiKeyUrl="https://platform.openai.com/api-keys"
151
+ adminApiKeyUrl="https://platform.openai.com/organization/admin-keys"
152
+ hasAdminKey
153
+ badgeIcon={Shield}
154
+ badgeTooltip="API Admin supportée"
155
+ />
156
+ ```
157
+
158
+ ### Usage TemplateCard (Templates)
159
+
160
+ ```tsx
161
+ <TemplateCard
162
+ name="Welcome Email"
163
+ code="welcome"
164
+ category="Transactional"
165
+ isActive
166
+ isSystem
167
+ icon={Mail}
168
+ iconColor="var(--color-accent-500)"
169
+ translationsCount={3}
170
+ onClick={() => navigate('/...')}
171
+ onEdit={() => navigate('/edit')}
172
+ onDelete={() => handleDelete()}
173
+ labels={{
174
+ activeLabel: t('emailTemplates.active'),
175
+ inactiveLabel: t('emailTemplates.inactive'),
176
+ systemLabel: 'System',
177
+ editLabel: t('emailTemplates.edit'),
178
+ deleteLabel: t('emailTemplates.delete'),
179
+ }}
180
+ />
181
+ ```
182
+
183
+ ### TemplateCard Props
184
+
185
+ | Prop | Type | Description |
186
+ |------|------|-------------|
187
+ | `name` | `string` | Nom du template (requis) |
188
+ | `code` | `string` | Code/slug du template (requis) |
189
+ | `category` | `string` | Catégorie affichée en tag |
190
+ | `isActive` | `boolean` | État actif/inactif (badge dans header) |
191
+ | `isSystem` | `boolean` | Template système (non supprimable) |
192
+ | `icon` | `ElementType` | Icône Lucide pour l'avatar |
193
+ | `iconColor` | `string` | Couleur de l'avatar (CSS) |
194
+ | `translationsCount` | `number` | Nombre de traductions |
195
+ | `onClick` | `() => void` | Click sur la card |
196
+ | `onEdit` | `() => void` | Bouton éditer |
197
+ | `onDelete` | `() => void` | Bouton supprimer (masqué si isSystem) |
198
+ | `labels` | `object` | Labels i18n pour tous les textes |
199
+
200
+ ---
201
+
202
+ ## PROPS RÉFÉRENCE
203
+
204
+ ### EntityCard Props
205
+
206
+ | Prop | Type | Description |
207
+ |------|------|-------------|
208
+ | `avatar` | `{ letter, color, imageUrl? }` | Avatar carré arrondi |
209
+ | `title` | `string` | Titre principal (requis) |
210
+ | `subtitle` | `string` | Code/slug en gris (affiché en `<code>`) |
211
+ | `description` | `string \| ReactNode` | Description (line-clamp-2) |
212
+ | `stats` | `string \| ReactNode` | Statistique (nombre en bold) |
213
+ | `badge` | `{ icon?, tooltip?, color? }` | Badge dans le header |
214
+ | `links` | `Array<{ icon, label, href?, onClick? }>` | Liens avec icônes |
215
+ | `actions` | `Array<{ label, href?, onClick?, variant, icon?, disabled?, loading? }>` | Boutons d'action |
216
+ | `tags` | `Array<{ label, variant, onClick? }>` | Tags/catégories |
217
+ | `onClick` | `() => void` | Click sur la card |
218
+ | `customHeader` | `ReactNode` | Remplace le header par défaut |
219
+ | `customBody` | `ReactNode` | Remplace le body par défaut |
220
+ | `customFooter` | `ReactNode` | Remplace le footer par défaut |
221
+
222
+ ### Action Props
223
+
224
+ | Prop | Type | Description |
225
+ |------|------|-------------|
226
+ | `label` | `string` | Texte du bouton |
227
+ | `href` | `string` | URL (rend un `<a>` au lieu de `<button>`) |
228
+ | `onClick` | `() => void` | Handler de click |
229
+ | `variant` | `'primary' \| 'secondary' \| 'ghost'` | Style du bouton |
230
+ | `icon` | `ElementType` | Icône Lucide |
231
+ | `disabled` | `boolean` | État désactivé |
232
+ | `loading` | `boolean` | État de chargement |
233
+
234
+ ### Action Variants
235
+
236
+ | Variant | Style |
237
+ |---------|-------|
238
+ | `primary` | `bg-accent-500 hover:bg-accent-600 text-white` |
239
+ | `secondary` | `bg-accent-700 hover:bg-accent-800 text-white` |
240
+ | `ghost` | `border border-border-color bg-transparent` |
241
+
242
+ ---
243
+
244
+ ## PATTERNS DE GRILLE
245
+
246
+ ### Grille Responsive Standard
247
+
248
+ ```tsx
249
+ <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
250
+ {items.map(item => (
251
+ <EntityCard key={item.id} {...mapToCardProps(item)} />
252
+ ))}
253
+ </div>
254
+ ```
255
+
256
+ ### Grille avec État Vide
257
+
258
+ ```tsx
259
+ {items.length === 0 ? (
260
+ <div className="col-span-full text-center py-12">
261
+ <Package className="w-12 h-12 mx-auto mb-4 opacity-50" />
262
+ <p className="text-[var(--text-secondary)]">Aucun élément trouvé</p>
263
+ </div>
264
+ ) : (
265
+ <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
266
+ {items.map(item => (
267
+ <EntityCard key={item.id} {...mapToCardProps(item)} />
268
+ ))}
269
+ </div>
270
+ )}
271
+ ```
272
+
273
+ ### Grille avec Loading
274
+
275
+ ```tsx
276
+ {loading ? (
277
+ <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
278
+ {Array.from({ length: 8 }).map((_, i) => (
279
+ <div key={i} className="bg-[var(--bg-card)] border-2 border-[var(--color-accent-200)] rounded-[var(--radius-card)] overflow-hidden animate-pulse">
280
+ <div className="bg-[var(--color-accent-50)] p-4 border-b border-[var(--color-accent-200)]">
281
+ <div className="flex items-center gap-3">
282
+ <div className="w-10 h-10 rounded-lg bg-[var(--bg-tertiary)]" />
283
+ <div className="flex-1">
284
+ <div className="h-4 bg-[var(--bg-tertiary)] rounded w-3/4 mb-2" />
285
+ <div className="h-3 bg-[var(--bg-tertiary)] rounded w-1/2" />
286
+ </div>
287
+ </div>
288
+ </div>
289
+ <div className="p-4">
290
+ <div className="h-3 bg-[var(--bg-tertiary)] rounded w-full mb-2" />
291
+ <div className="h-3 bg-[var(--bg-tertiary)] rounded w-2/3" />
292
+ </div>
293
+ </div>
294
+ ))}
295
+ </div>
296
+ ) : (
297
+ // ... actual grid
298
+ )}
299
+ ```
300
+
301
+ ---
302
+
303
+ ## WORKFLOW DE GÉNÉRATION
304
+
305
+ ### ÉTAPE 1: Identifier le Type d'Entité
306
+
307
+ | Type | Composant | Exemple |
308
+ |------|-----------|---------|
309
+ | Provider IA | `ProviderCard` | OpenAI, Anthropic |
310
+ | Template | `TemplateCard` | Email templates |
311
+ | Entité standard | `EntityCard` | Produits, Utilisateurs |
312
+ | Item avec actions | `EntityCard` + actions | Tickets, Commandes |
313
+
314
+ ### ÉTAPE 2: Mapper les Props
315
+
316
+ ```typescript
317
+ // Pattern de mapping
318
+ function mapEntityToCard(entity: Entity): EntityCardProps {
319
+ return {
320
+ avatar: {
321
+ letter: entity.name[0].toUpperCase(),
322
+ color: getColorForType(entity.type),
323
+ },
324
+ title: entity.name,
325
+ subtitle: entity.code,
326
+ description: entity.description,
327
+ stats: entity.itemCount ? `${entity.itemCount} élément(s)` : undefined,
328
+ badge: entity.hasSpecialFeature ? { icon: Star, tooltip: 'Feature spéciale' } : undefined,
329
+ links: entity.websiteUrl ? [
330
+ { icon: ExternalLink, label: 'Site web', href: entity.websiteUrl }
331
+ ] : undefined,
332
+ actions: [
333
+ { label: 'Voir détails', onClick: () => navigate(`/${entity.id}`), variant: 'primary' }
334
+ ],
335
+ };
336
+ }
337
+ ```
338
+
339
+ ### ÉTAPE 3: Utiliser dans la Page
340
+
341
+ ```tsx
342
+ // Pattern page complète
343
+ export function EntitiesPage() {
344
+ const [entities, setEntities] = useState<Entity[]>([]);
345
+ const [loading, setLoading] = useState(true);
346
+
347
+ useEffect(() => {
348
+ loadEntities();
349
+ }, []);
350
+
351
+ return (
352
+ <div className="space-y-6">
353
+ <PageHeader title="Entités" subtitle="Gérer les entités" />
354
+
355
+ <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
356
+ {entities.map(entity => (
357
+ <EntityCard key={entity.id} {...mapEntityToCard(entity)} />
358
+ ))}
359
+ </div>
360
+ </div>
361
+ );
362
+ }
363
+ ```
364
+
365
+ ---
366
+
367
+ ## QUAND UTILISER EntityCard vs Custom Cards
368
+
369
+ ### Utiliser EntityCard pour:
370
+ - Listes d'entités homogènes (produits, providers, templates)
371
+ - Cards avec structure standard: avatar, titre, description, liens, actions
372
+ - Grilles de cards cliquables
373
+ - Affichage de catalogues
374
+
375
+ ### NE PAS utiliser EntityCard pour:
376
+ - Dashboard stats cards (sync status, métriques)
377
+ - Cards avec états interactifs complexes (sélection, toggle inline)
378
+ - Cards avec sous-listes intégrées (ex: roles assignés avec X pour supprimer)
379
+ - Cards master/detail avec panneau de détails
380
+ - Cards avec formulaires intégrés
381
+
382
+ ---
383
+
384
+ ## PATTERN: CUSTOM CARDS (Status, Stats, Dashboard)
385
+
386
+ Pour les cards qui ne peuvent pas utiliser `EntityCard`, utiliser ce pattern pour garantir l'alignement des boutons en bas.
387
+
388
+ ### Structure de Base avec Alignement Boutons
389
+
390
+ ```tsx
391
+ // ⚠️ OBLIGATOIRE: h-full flex flex-col sur le container
392
+ <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">
393
+
394
+ {/* Header avec gradient */}
395
+ <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)]">
396
+ <div className="flex items-center justify-between">
397
+ <div className="flex items-center gap-3">
398
+ <div className="p-2 rounded-lg bg-[var(--color-accent-500)]/20">
399
+ <Icon className="w-5 h-5 text-[var(--color-accent-600)]" />
400
+ </div>
401
+ <h3 className="font-semibold text-[var(--text-primary)]">
402
+ {title}
403
+ </h3>
404
+ </div>
405
+ {/* Badge de statut (optionnel) */}
406
+ <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)]">
407
+ <StatusIcon className="w-3.5 h-3.5" />
408
+ {statusLabel}
409
+ </div>
410
+ </div>
411
+ </div>
412
+
413
+ {/* ⚠️ OBLIGATOIRE: flex-1 flex flex-col pour que le contenu prenne toute la hauteur disponible */}
414
+ <div className="flex-1 flex flex-col p-4">
415
+
416
+ {/* Stats Grid (optionnel) */}
417
+ <div className="grid grid-cols-3 gap-3 mb-4">
418
+ <StatBlock icon={Database} value={123} label="Items" />
419
+ <StatBlock icon={Calendar} value="15 jan" label="Last sync" />
420
+ <StatBlock icon={RefreshCw} value="14 jan" label="Delta sync" />
421
+ </div>
422
+
423
+ {/* Contenu variable (warnings, errors, etc.) */}
424
+ {warning && (
425
+ <div className="p-3 mb-4 rounded-lg bg-[var(--warning-bg)] border border-[var(--warning-border)]">
426
+ <span className="text-sm text-[var(--warning-text)]">{warning}</span>
427
+ </div>
428
+ )}
429
+
430
+ {/* ⚠️ OBLIGATOIRE: mt-auto pour pousser le bouton en bas */}
431
+ <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">
432
+ <RefreshCw className="w-4 h-4" />
433
+ {actionLabel}
434
+ </button>
435
+ </div>
436
+ </div>
437
+ ```
438
+
439
+ ### Règles d'Alignement Boutons (CRITIQUES)
440
+
441
+ | Élément | Classe CSS | Raison |
442
+ |---------|------------|--------|
443
+ | **Card container** | `h-full flex flex-col` | Permet à la card de remplir la hauteur de la grille |
444
+ | **Content wrapper** | `flex-1 flex flex-col` | Le contenu prend l'espace disponible |
445
+ | **Action button** | `mt-auto` | Pousse le bouton tout en bas |
446
+
447
+ ### Exemple Complet: Sync Status Card
448
+
449
+ ```tsx
450
+ interface SyncStatusCardProps {
451
+ resourceType: string;
452
+ icon: LucideIcon;
453
+ status: 'Success' | 'Failed' | 'Running' | 'Pending';
454
+ lastSyncAt: string | null;
455
+ itemCount: number;
456
+ pendingConflicts: number;
457
+ error: string | null;
458
+ syncing: boolean;
459
+ onSync: () => void;
460
+ }
461
+
462
+ function SyncStatusCard({
463
+ resourceType, icon: Icon, status, lastSyncAt, itemCount,
464
+ pendingConflicts, error, syncing, onSync
465
+ }: SyncStatusCardProps) {
466
+
467
+ const statusConfig = getStatusConfig(status); // Returns { icon, color, bg, label }
468
+
469
+ return (
470
+ <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">
471
+ {/* Header */}
472
+ <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)]">
473
+ <div className="flex items-center justify-between">
474
+ <div className="flex items-center gap-3">
475
+ <div className="p-2 rounded-lg bg-[var(--color-accent-500)]/20">
476
+ <Icon className="w-5 h-5 text-[var(--color-accent-600)]" />
477
+ </div>
478
+ <h3 className="font-semibold text-[var(--text-primary)]">{resourceType}</h3>
479
+ </div>
480
+ <div className={`inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full text-xs font-medium ${statusConfig.bg} ${statusConfig.color}`}>
481
+ <statusConfig.icon className="w-3.5 h-3.5" />
482
+ {statusConfig.label}
483
+ </div>
484
+ </div>
485
+ </div>
486
+
487
+ {/* Content */}
488
+ <div className="flex-1 flex flex-col p-4">
489
+ {/* Stats */}
490
+ <div className="grid grid-cols-2 gap-3 mb-4">
491
+ <div className="text-center p-3 rounded-lg bg-[var(--bg-secondary)]">
492
+ <p className="text-xl font-bold">{itemCount}</p>
493
+ <p className="text-xs text-[var(--text-tertiary)]">Synced</p>
494
+ </div>
495
+ <div className="text-center p-3 rounded-lg bg-[var(--bg-secondary)]">
496
+ <p className="text-sm font-medium">{formatDate(lastSyncAt)}</p>
497
+ <p className="text-xs text-[var(--text-tertiary)]">Last sync</p>
498
+ </div>
499
+ </div>
500
+
501
+ {/* Warnings (variable height) */}
502
+ {pendingConflicts > 0 && (
503
+ <div className="p-3 mb-4 rounded-lg bg-[var(--warning-bg)] border border-[var(--warning-border)]">
504
+ <span className="text-sm text-[var(--warning-text)]">
505
+ {pendingConflicts} conflicts pending
506
+ </span>
507
+ </div>
508
+ )}
509
+
510
+ {/* Errors (variable height) */}
511
+ {error && (
512
+ <div className="p-3 mb-4 rounded-lg bg-[var(--error-bg)] border border-[var(--error-border)]">
513
+ <p className="text-xs text-[var(--error-text)] line-clamp-2">{error}</p>
514
+ </div>
515
+ )}
516
+
517
+ {/* Button - ALWAYS at bottom */}
518
+ <button
519
+ onClick={onSync}
520
+ disabled={syncing}
521
+ 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"
522
+ >
523
+ {syncing ? <Loader2 className="w-4 h-4 animate-spin" /> : <RefreshCw className="w-4 h-4" />}
524
+ Sync Now
525
+ </button>
526
+ </div>
527
+ </div>
528
+ );
529
+ }
530
+ ```
531
+
532
+ ### Usage dans une Grille
533
+
534
+ ```tsx
535
+ {/* La grille DOIT utiliser des colonnes égales */}
536
+ <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
537
+ {syncStatuses.map((status) => (
538
+ <SyncStatusCard
539
+ key={status.resourceType}
540
+ {...status}
541
+ onSync={() => handleSync(status.resourceType)}
542
+ />
543
+ ))}
544
+ </div>
545
+ ```
546
+
547
+ ### Pattern: Card Footer avec Métadonnées + Action
548
+
549
+ Quand une card a besoin d'afficher des métadonnées (date, statut) ET un bouton d'action en bas :
550
+
551
+ ```tsx
552
+ {/* ⚠️ OBLIGATOIRE: mt-auto sur le footer pour l'aligner en bas */}
553
+ <div className="mt-auto pt-4 border-t border-[var(--border-color)]">
554
+ <div className="flex items-center justify-between">
555
+ {/* Métadonnées à gauche */}
556
+ <div className="flex items-center gap-1.5 text-xs text-[var(--text-tertiary)]">
557
+ <Calendar className="w-3.5 h-3.5" />
558
+ {formatDate(createdAt)}
559
+ </div>
560
+
561
+ {/* Bouton d'action à droite */}
562
+ <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">
563
+ <CheckCircle className="w-4 h-4" />
564
+ {actionLabel}
565
+ </button>
566
+ </div>
567
+ </div>
568
+ ```
569
+
570
+ **Structure complète avec footer :**
571
+
572
+ ```tsx
573
+ <div className="h-full flex flex-col rounded-[var(--radius-card)] border border-[var(--border-color)] bg-[var(--bg-card)] overflow-hidden">
574
+ {/* Header */}
575
+ <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)]">
576
+ {/* ... */}
577
+ </div>
578
+
579
+ {/* Content - flex-1 flex flex-col pour occuper l'espace */}
580
+ <div className="flex-1 flex flex-col p-4 space-y-4">
581
+ {/* Contenu variable */}
582
+ <p className="text-sm text-[var(--text-secondary)]">{description}</p>
583
+
584
+ {/* Composants additionnels */}
585
+ <SomeComponent />
586
+
587
+ {/* ⚠️ Footer avec mt-auto - TOUJOURS en dernier dans le content */}
588
+ <div className="mt-auto flex items-center justify-between pt-4 border-t border-[var(--border-color)]">
589
+ <div className="flex items-center gap-1.5 text-xs text-[var(--text-tertiary)]">
590
+ <Calendar className="w-3.5 h-3.5" />
591
+ {date}
592
+ </div>
593
+ <button className="...">Action</button>
594
+ </div>
595
+ </div>
596
+ </div>
597
+ ```
598
+
599
+ ### ⚠️ Erreurs Communes à Éviter
600
+
601
+ ```tsx
602
+ // ❌ MAUVAIS - Les boutons ne seront PAS alignés
603
+ <div className="rounded-lg border ...">
604
+ <div className="p-4">
605
+ {/* contenu */}
606
+ <button>Action</button> {/* Pas de mt-auto, pas d'alignement */}
607
+ </div>
608
+ </div>
609
+
610
+ // ❌ MAUVAIS - Footer sans mt-auto
611
+ <div className="h-full flex flex-col ...">
612
+ <div className="flex-1 flex flex-col p-4">
613
+ {/* contenu */}
614
+ <div className="pt-4 border-t ..."> {/* Manque mt-auto ! */}
615
+ <button>Action</button>
616
+ </div>
617
+ </div>
618
+ </div>
619
+
620
+ // ✅ BON - Footer aligné en bas avec mt-auto
621
+ <div className="h-full flex flex-col rounded-lg border ...">
622
+ <div className="flex-1 flex flex-col p-4">
623
+ {/* contenu */}
624
+ <div className="mt-auto pt-4 border-t ..."> {/* mt-auto présent */}
625
+ <div className="flex items-center justify-between">
626
+ <span>{metadata}</span>
627
+ <button>Action</button>
628
+ </div>
629
+ </div>
630
+ </div>
631
+ </div>
632
+ ```
633
+
634
+ ---
635
+
636
+ ## RÈGLES ABSOLUES
637
+
638
+ 1. **TOUJOURS** utiliser `EntityCard` pour les cards d'entités
639
+ 2. **TOUJOURS** utiliser le header coloré distinct
640
+ 3. **TOUJOURS** utiliser `rounded-lg` pour l'avatar (carré arrondi)
641
+ 4. **TOUJOURS** utiliser les variables CSS d'accent (`--color-accent-*`)
642
+ 5. **TOUJOURS** grille responsive (1→2→3→4 colonnes)
643
+ 6. **TOUJOURS** gérer les états vide et loading
644
+ 7. **TOUJOURS** utiliser `href` pour les liens externes (ouvre nouvel onglet)
645
+ 8. **TOUJOURS** utiliser `h-full flex flex-col` + `flex-1` + `mt-auto` pour aligner les boutons en bas des custom cards
646
+ 9. **JAMAIS** de `rounded-full` pour l'avatar (c'est le design DataTable/table)
647
+ 10. **JAMAIS** de cards custom en divs manuels pour les entités simples
648
+ 11. **JAMAIS** de grille fixe non-responsive
649
+ 12. **JAMAIS** de boutons non alignés dans une grille de cards (utiliser le pattern d'alignement)
650
+
651
+ ---
652
+
653
+ ## COMPOSANT: DataTable
654
+
655
+ **Fichier:** `web/smartstack-web/src/components/ui/DataTable.tsx`
656
+
657
+ ### Import
658
+
659
+ ```typescript
660
+ import { DataTable } from '@/components/ui/DataTable';
661
+ ```
662
+
663
+ ### Usage
664
+
665
+ ```tsx
666
+ <DataTable
667
+ data={users}
668
+ columns={[
669
+ { key: 'name', label: 'Nom', sortable: true },
670
+ { key: 'email', label: 'Email', sortable: true },
671
+ { key: 'role', label: 'Rôle', render: (user) => <Badge>{user.role}</Badge> },
672
+ { key: 'createdAt', label: 'Créé le', render: (user) => formatDate(user.createdAt) },
673
+ ]}
674
+ pagination={{ pageSize: 10, showSizeSelector: true }}
675
+ searchable
676
+ searchPlaceholder="Rechercher un utilisateur..."
677
+ onRowClick={(user) => navigate(`/users/${user.id}`)}
678
+ getRowKey={(user) => user.id}
679
+ striped
680
+ />
681
+ ```
682
+
683
+ ### Props Principales
684
+
685
+ | Prop | Type | Description |
686
+ |------|------|-------------|
687
+ | `data` | `T[]` | Données à afficher |
688
+ | `columns` | `DataTableColumn<T>[]` | Définition des colonnes |
689
+ | `pagination` | `{ pageSize, showSizeSelector? }` | Config pagination |
690
+ | `searchable` | `boolean` | Activer recherche globale |
691
+ | `onRowClick` | `(item, index) => void` | Click sur ligne |
692
+ | `selectable` | `boolean` | Activer sélection multi |
693
+ | `loading` | `boolean` | État chargement |
694
+ | `striped` | `boolean` | Lignes alternées |
695
+ | `compact` | `boolean` | Mode compact |
696
+
697
+ ### Column Config
698
+
699
+ ```typescript
700
+ interface DataTableColumn<T> {
701
+ key: string; // Clé de l'objet ou custom
702
+ label: string; // Label en-tête
703
+ sortable?: boolean; // Tri activé
704
+ render?: (item: T) => ReactNode; // Rendu custom
705
+ width?: string; // Largeur CSS
706
+ align?: 'left' | 'center' | 'right';
707
+ hideOnMobile?: boolean;
708
+ }
709
+ ```
710
+
711
+ ---
712
+
713
+ ## COMPOSANT: Tooltip
714
+
715
+ **Fichier:** `web/smartstack-web/src/components/ui/Tooltip.tsx`
716
+
717
+ ### Import
718
+
719
+ ```typescript
720
+ import { Tooltip, type TooltipVariant, type TooltipPosition } from '@/components/ui/Tooltip';
721
+ ```
722
+
723
+ ### Variantes Disponibles
724
+
725
+ | Variant | Couleur | Usage |
726
+ |---------|---------|-------|
727
+ | `default` | Gris (thème) | Information neutre |
728
+ | `error` | Rouge | Permission refusée, erreur, action interdite |
729
+ | `warning` | Orange | Avertissement, action irréversible |
730
+ | `success` | Vert | Confirmation, action réussie |
731
+ | `info` | Bleu | Information contextuelle, aide |
732
+
733
+ ### Positions Disponibles
734
+
735
+ | Position | Description |
736
+ |----------|-------------|
737
+ | `top` | Au-dessus de l'élément (défaut) |
738
+ | `bottom` | En dessous de l'élément |
739
+ | `left` | À gauche de l'élément |
740
+ | `right` | À droite de l'élément |
741
+
742
+ ### Usage de Base
743
+
744
+ ```tsx
745
+ import { Tooltip } from '@/components/ui/Tooltip';
746
+
747
+ // Tooltip par défaut (informatif)
748
+ <Tooltip content="Information contextuelle">
749
+ <button>Hover me</button>
750
+ </Tooltip>
751
+
752
+ // Tooltip d'erreur (permission refusée)
753
+ <Tooltip
754
+ content="Action non autorisée - permission 'execute' requise"
755
+ variant="error"
756
+ position="top"
757
+ >
758
+ <button disabled>Action protégée</button>
759
+ </Tooltip>
760
+
761
+ // Tooltip de warning (action dangereuse)
762
+ <Tooltip
763
+ content="Cette action est irréversible"
764
+ variant="warning"
765
+ position="bottom"
766
+ >
767
+ <button>Supprimer</button>
768
+ </Tooltip>
769
+
770
+ // Tooltip de succès
771
+ <Tooltip content="Fichier sauvegardé" variant="success">
772
+ <span>✓ Saved</span>
773
+ </Tooltip>
774
+
775
+ // Tooltip informatif
776
+ <Tooltip content="Cliquez pour plus de détails" variant="info">
777
+ <HelpCircle className="w-4 h-4" />
778
+ </Tooltip>
779
+ ```
780
+
781
+ ### Pattern: Bouton Désactivé avec Explication
782
+
783
+ ```tsx
784
+ // Pattern recommandé pour les boutons désactivés avec permission
785
+ const { hasPermission } = useAuth();
786
+ const canExecute = hasPermission('module.action.execute');
787
+
788
+ <Tooltip
789
+ content={!canExecute ? t('errors.noPermission') : undefined}
790
+ variant="error"
791
+ disabled={canExecute} // Tooltip ne s'affiche que si désactivé
792
+ >
793
+ <button
794
+ onClick={handleAction}
795
+ disabled={!canExecute}
796
+ className="... disabled:opacity-50 disabled:cursor-not-allowed"
797
+ >
798
+ Action
799
+ </button>
800
+ </Tooltip>
801
+ ```
802
+
803
+ ### Props Complètes
804
+
805
+ | Prop | Type | Default | Description |
806
+ |------|------|---------|-------------|
807
+ | `content` | `ReactNode` | - | Contenu du tooltip (requis) |
808
+ | `children` | `ReactNode` | - | Élément déclencheur (requis) |
809
+ | `position` | `'top' \| 'bottom' \| 'left' \| 'right'` | `'top'` | Position du tooltip |
810
+ | `variant` | `'default' \| 'error' \| 'warning' \| 'success' \| 'info'` | `'default'` | Style/couleur du tooltip |
811
+ | `delay` | `number` | `200` | Délai avant affichage (ms) |
812
+ | `disabled` | `boolean` | `false` | Désactive le tooltip |
813
+ | `className` | `string` | `''` | Classes CSS additionnelles |
814
+
815
+ ### Variables CSS Utilisées
816
+
817
+ Le tooltip utilise des variables CSS opaques pour garantir la lisibilité :
818
+
819
+ ```css
820
+ /* Light mode */
821
+ --tooltip-error-bg: #fef2f2;
822
+ --tooltip-warning-bg: #fefce8;
823
+ --tooltip-success-bg: #f0fdf4;
824
+ --tooltip-info-bg: #eff6ff;
825
+
826
+ /* Dark mode */
827
+ --tooltip-error-bg: #3b1c1e;
828
+ --tooltip-warning-bg: #3b2f1a;
829
+ --tooltip-success-bg: #1a3329;
830
+ --tooltip-info-bg: #1a2c3b;
831
+ ```
832
+
833
+ ### ⚠️ Règles d'Usage
834
+
835
+ 1. **TOUJOURS** utiliser `variant="error"` pour les permissions refusées
836
+ 2. **TOUJOURS** utiliser `variant="warning"` pour les actions dangereuses/irréversibles
837
+ 3. **TOUJOURS** désactiver le tooltip (`disabled={true}`) quand l'action est autorisée
838
+ 4. **TOUJOURS** positionner le tooltip de manière à ne pas bloquer les éléments importants
839
+ 5. **JAMAIS** utiliser le tooltip natif `title` HTML - utiliser ce composant à la place
840
+ 6. **JAMAIS** de fonds transparents - le composant utilise des fonds opaques
841
+
842
+ ---
843
+
844
+ ## COMPOSANTS - RÉSUMÉ
845
+
846
+ | Composant | Fichier | Usage |
847
+ |-----------|---------|-------|
848
+ | **EntityCard** | `EntityCard.tsx` | Cards d'entités avec header coloré |
849
+ | **ProviderCard** | `EntityCard.tsx` | Preset pour providers IA |
850
+ | **TemplateCard** | `EntityCard.tsx` | Preset pour templates |
851
+ | **DataTable** | `DataTable.tsx` | Tableaux avec tri/pagination/recherche |
852
+ | **Tooltip** | `Tooltip.tsx` | Tooltips contextuels avec variantes colorées |
853
+
854
+ ---
855
+
856
+ ## VALIDATION
857
+
858
+ Après génération, vérifier :
859
+
860
+ ```bash
861
+ # TypeScript
862
+ cd web/smartstack-web && npx tsc --noEmit
863
+
864
+ # Lint
865
+ npm run lint
866
+
867
+ # Import correct
868
+ grep -r "EntityCard" src/pages/ | grep -v "from '@/components/ui/EntityCard'"
869
+ # ↑ Doit retourner vide (tous imports corrects)
870
+ ```