@atlashub/smartstack-cli 1.4.1 → 1.5.1
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.
- package/.documentation/agents.html +5 -1
- package/.documentation/apex.html +5 -1
- package/.documentation/business-analyse.html +5 -1
- package/.documentation/commands.html +5 -1
- package/.documentation/css/styles.css +2168 -2168
- package/.documentation/efcore.html +5 -1
- package/.documentation/gitflow.html +5 -1
- package/.documentation/hooks.html +5 -1
- package/.documentation/index.html +5 -1
- package/.documentation/init.html +565 -0
- package/.documentation/installation.html +92 -6
- package/.documentation/js/app.js +794 -794
- package/.documentation/ralph-loop.html +534 -530
- package/.documentation/test-web.html +5 -1
- package/dist/index.js +817 -277
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/templates/agents/efcore/conflicts.md +44 -17
- package/templates/agents/efcore/db-status.md +27 -6
- package/templates/agents/efcore/scan.md +43 -13
- package/templates/commands/ai-prompt.md +315 -315
- package/templates/commands/application/create.md +362 -362
- package/templates/commands/controller/create.md +216 -216
- package/templates/commands/controller.md +59 -0
- package/templates/commands/documentation/module.md +202 -202
- package/templates/commands/efcore/_env-check.md +153 -153
- package/templates/commands/efcore/conflicts.md +109 -192
- package/templates/commands/efcore/db-status.md +101 -89
- package/templates/commands/efcore/migration.md +23 -11
- package/templates/commands/efcore/scan.md +115 -119
- package/templates/commands/efcore.md +54 -6
- package/templates/commands/feature-full.md +267 -267
- package/templates/commands/gitflow/11-finish.md +145 -11
- package/templates/commands/gitflow/13-sync.md +216 -216
- package/templates/commands/gitflow/14-rebase.md +251 -251
- package/templates/commands/gitflow/2-status.md +120 -10
- package/templates/commands/gitflow/3-commit.md +150 -0
- package/templates/commands/gitflow/7-pull-request.md +134 -5
- package/templates/commands/gitflow/9-merge.md +142 -1
- package/templates/commands/implement.md +663 -663
- package/templates/commands/init.md +567 -0
- package/templates/commands/mcp-integration.md +330 -0
- package/templates/commands/notification.md +129 -129
- package/templates/commands/validate.md +233 -0
- package/templates/commands/workflow.md +193 -193
- package/templates/skills/ai-prompt/SKILL.md +778 -778
- package/templates/skills/application/SKILL.md +563 -563
- package/templates/skills/application/templates-backend.md +450 -450
- package/templates/skills/application/templates-frontend.md +531 -531
- package/templates/skills/application/templates-i18n.md +520 -520
- package/templates/skills/application/templates-seed.md +647 -647
- package/templates/skills/controller/SKILL.md +240 -240
- package/templates/skills/controller/postman-templates.md +614 -614
- package/templates/skills/controller/templates.md +1468 -1468
- package/templates/skills/documentation/SKILL.md +133 -133
- package/templates/skills/documentation/templates.md +476 -476
- package/templates/skills/feature-full/SKILL.md +838 -838
- package/templates/skills/notification/SKILL.md +555 -555
- package/templates/skills/ui-components/SKILL.md +870 -870
- 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
|
+
```
|