@synertech/ui 0.39.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.
- package/.claude/skills/use-synertech-ui/SKILL.md +1578 -0
- package/LICENSE +37 -0
- package/README.md +343 -0
- package/bin/setup-claude-skill.mjs +61 -0
- package/dist/index.cjs +31 -0
- package/dist/index.d.cts +2433 -0
- package/dist/index.d.ts +2433 -0
- package/dist/index.js +31 -0
- package/dist/styles.css +6863 -0
- package/package.json +112 -0
|
@@ -0,0 +1,1578 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: use-synertech-ui
|
|
3
|
+
description: Guide for optimal usage of @synertech/ui components in consuming projects. Provides component API reference, composition patterns, setup instructions, and anti-patterns to avoid.
|
|
4
|
+
allowed-tools: Read, Glob, Grep, Write, Edit, Bash(pnpm:*), Bash(npx:*), Bash(npm:*), Agent
|
|
5
|
+
argument-hint: "[component-name or question]"
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# @synertech/ui - Guide d'utilisation optimale
|
|
9
|
+
|
|
10
|
+
Ce skill guide les développeurs pour utiliser correctement et efficacement la librairie `@synertech/ui` dans leurs projets Next.js/React.
|
|
11
|
+
|
|
12
|
+
## Arguments
|
|
13
|
+
|
|
14
|
+
Format : `[component-name]` ou question libre
|
|
15
|
+
|
|
16
|
+
Exemples :
|
|
17
|
+
- `/use-synertech-ui Button` - Guide d'utilisation du Button
|
|
18
|
+
- `/use-synertech-ui Dropdown` - Guide d'utilisation du Dropdown
|
|
19
|
+
- `/use-synertech-ui comment faire un formulaire` - Question libre
|
|
20
|
+
- `/use-synertech-ui setup` - Instructions d'installation
|
|
21
|
+
|
|
22
|
+
Arguments fournis : $ARGUMENTS
|
|
23
|
+
|
|
24
|
+
## Comportement
|
|
25
|
+
|
|
26
|
+
1. Si un **nom de composant** est fourni : afficher l'API complète, les exemples d'utilisation idiomatiques, et les anti-patterns à éviter
|
|
27
|
+
2. Si une **question** est posée : répondre en utilisant les composants disponibles de la librairie
|
|
28
|
+
3. Si `setup` est fourni : afficher les instructions d'installation complètes
|
|
29
|
+
4. Sans argument : afficher le catalogue des composants disponibles avec un résumé
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## Installation & Configuration
|
|
34
|
+
|
|
35
|
+
### 1. Installer le package
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
pnpm add @synertech/ui
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Le package est publié sur le registre npm public : aucun `.npmrc` ni token à configurer.
|
|
42
|
+
|
|
43
|
+
### 2. Importer les styles
|
|
44
|
+
|
|
45
|
+
Dans le fichier d'entrée de l'application (ex: `layout.tsx` ou `_app.tsx`) :
|
|
46
|
+
|
|
47
|
+
```tsx
|
|
48
|
+
import "@synertech/ui/styles.css";
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### 3. Peer Dependencies
|
|
52
|
+
|
|
53
|
+
React >= 18 et React-DOM >= 18 doivent etre installés dans le projet consommateur.
|
|
54
|
+
|
|
55
|
+
`@mdxeditor/editor` est une peer dependency **optionnelle**, utilisée uniquement par
|
|
56
|
+
`<HelpButton markdown editable>`. Sans elle, le mode édition affiche un message et tout
|
|
57
|
+
le reste de la librairie fonctionne normalement. Pour l'activer :
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
pnpm add @mdxeditor/editor
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
```tsx
|
|
64
|
+
import "@mdxeditor/editor/style.css";
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
## Imports
|
|
70
|
+
|
|
71
|
+
Tous les composants sont exportés depuis le point d'entrée unique :
|
|
72
|
+
|
|
73
|
+
```tsx
|
|
74
|
+
import { Button, Dropdown, DropdownTrigger, DropdownContent, Select, Card } from "@synertech/ui";
|
|
75
|
+
import type { ButtonProps, SelectOption } from "@synertech/ui";
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
L'utilitaire `cn()` pour merger des classes Tailwind est aussi exporté :
|
|
79
|
+
|
|
80
|
+
```tsx
|
|
81
|
+
import { cn } from "@synertech/ui";
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
## Catalogue des composants
|
|
87
|
+
|
|
88
|
+
### Display & Media
|
|
89
|
+
|
|
90
|
+
| Composant | Description | Server-compatible |
|
|
91
|
+
|-----------|-------------|:-:|
|
|
92
|
+
| `Avatar` | Photo de profil avec fallback texte | Oui |
|
|
93
|
+
| `Skeleton` | Placeholder de chargement (pulse/wave) | Oui |
|
|
94
|
+
| `Indicator` | Badge de notification (dot/count/status/legend) | Oui |
|
|
95
|
+
|
|
96
|
+
### Navigation & Actions
|
|
97
|
+
|
|
98
|
+
| Composant | Description | Server-compatible |
|
|
99
|
+
|-----------|-------------|:-:|
|
|
100
|
+
| `Button` | Bouton avec variantes de taille, couleur, icones | Oui |
|
|
101
|
+
| `ButtonGroup` | Groupe de boutons avec sélection | Non |
|
|
102
|
+
| `Breadcrumb` | Fil d'Ariane | Oui |
|
|
103
|
+
| `Pagination` | Navigation par pages | Non |
|
|
104
|
+
| `Dropdown` | Menu déroulant composable | Non |
|
|
105
|
+
| `Tabs` | Interface à onglets composable | Non |
|
|
106
|
+
|
|
107
|
+
### Forms & Input
|
|
108
|
+
|
|
109
|
+
| Composant | Description | Server-compatible |
|
|
110
|
+
|-----------|-------------|:-:|
|
|
111
|
+
| `Input` | Champ texte/textarea avec label | Non |
|
|
112
|
+
| `Select` | Combobox searchable avec portal | Non |
|
|
113
|
+
| `Checkbox` | Case à cocher | Non |
|
|
114
|
+
| `RadioButton` | Bouton radio | Non |
|
|
115
|
+
| `Slider` | Curseur (range) accessible avec marques et tooltip | Non |
|
|
116
|
+
| `ImageUploader` | Upload d'image (drag & drop, URL) | Non |
|
|
117
|
+
| `FileSelector` | Sélection de fichiers/dossiers (drag & drop, validation) | Non |
|
|
118
|
+
|
|
119
|
+
### Layout & Structure
|
|
120
|
+
|
|
121
|
+
| Composant | Description | Server-compatible |
|
|
122
|
+
|-----------|-------------|:-:|
|
|
123
|
+
| `Card` | Carte composable (Header, Body, Footer) | Oui |
|
|
124
|
+
| `Header` | En-tête de page avec navigation | Non |
|
|
125
|
+
| `Footer` | Pied de page | Oui |
|
|
126
|
+
| `Drawer` | Panneau latéral coulissant | Non |
|
|
127
|
+
| `Divider` | Séparateur horizontal/vertical | Oui |
|
|
128
|
+
|
|
129
|
+
### Data Display
|
|
130
|
+
|
|
131
|
+
| Composant | Description | Server-compatible |
|
|
132
|
+
|-----------|-------------|:-:|
|
|
133
|
+
| `Table` | Tableau composable (Header, Body, Row, Cell) | Oui |
|
|
134
|
+
| `DataGrid` | Tableau avancé data-driven (tri, pagination, filtres, sélection, export) | Non |
|
|
135
|
+
| `Chip` | Tag/badge | Oui |
|
|
136
|
+
| `Label` | Étiquette de formulaire | Oui |
|
|
137
|
+
| `Banner` | Bannière notification/hero/promotion | Non |
|
|
138
|
+
|
|
139
|
+
### Feedback & Overlays
|
|
140
|
+
|
|
141
|
+
| Composant | Description | Server-compatible |
|
|
142
|
+
|-----------|-------------|:-:|
|
|
143
|
+
| `Modal` | Dialog/overlay | Non |
|
|
144
|
+
| `Tooltip` | Info-bulle au survol/clic | Non |
|
|
145
|
+
| `Toast` | Notification temporaire | Non |
|
|
146
|
+
| `CircularProgress` | Indicateur de chargement | Oui |
|
|
147
|
+
| `HelpButton` | Bouton d'aide contextuel avec Markdown | Non |
|
|
148
|
+
|
|
149
|
+
### Advanced
|
|
150
|
+
|
|
151
|
+
| Composant | Description | Server-compatible |
|
|
152
|
+
|-----------|-------------|:-:|
|
|
153
|
+
| `Detachable` | Portail popup (window.open + createPortal) | Non |
|
|
154
|
+
| `AdvanceDetachable` | Detachable avancé (detach/clone, BroadcastChannel) | Non |
|
|
155
|
+
| `WorkflowEditor` | Editeur de workflow visuel (nodes/edges, pan/zoom) | Non |
|
|
156
|
+
| `SchemaEditor` | Editeur de schéma de base de données visuel | Non |
|
|
157
|
+
| `DeviceMockup` | Cadre simulant l'apparence d'appareils (phone, tablet, laptop, etc.) | Non |
|
|
158
|
+
|
|
159
|
+
---
|
|
160
|
+
|
|
161
|
+
## API Reference par composant
|
|
162
|
+
|
|
163
|
+
### Button
|
|
164
|
+
|
|
165
|
+
```tsx
|
|
166
|
+
import { Button } from "@synertech/ui";
|
|
167
|
+
|
|
168
|
+
<Button
|
|
169
|
+
size="md" // "sm" | "md" | "lg" | "xl"
|
|
170
|
+
rounded="lg" // "none" | "sm" | "md" | "lg" | "full"
|
|
171
|
+
bgColor="bg-blue-600 hover:bg-blue-700"
|
|
172
|
+
textColor="text-white"
|
|
173
|
+
focusColor="focus:ring-blue-300"
|
|
174
|
+
loading={false}
|
|
175
|
+
fullWidth={false}
|
|
176
|
+
selected={false}
|
|
177
|
+
leftIcon={<Icon />}
|
|
178
|
+
rightIcon={<Icon />}
|
|
179
|
+
icon={<Icon />} // Pour bouton icone seul
|
|
180
|
+
onClick={handleClick}
|
|
181
|
+
>
|
|
182
|
+
Label
|
|
183
|
+
</Button>
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
### ButtonGroup
|
|
187
|
+
|
|
188
|
+
```tsx
|
|
189
|
+
import { Button, ButtonGroup } from "@synertech/ui";
|
|
190
|
+
|
|
191
|
+
<ButtonGroup
|
|
192
|
+
orientation="horizontal" // "horizontal" | "vertical"
|
|
193
|
+
size="md"
|
|
194
|
+
selectedValue={selected}
|
|
195
|
+
onChange={setSelected}
|
|
196
|
+
>
|
|
197
|
+
<Button value="a">Option A</Button>
|
|
198
|
+
<Button value="b">Option B</Button>
|
|
199
|
+
<Button value="c">Option C</Button>
|
|
200
|
+
</ButtonGroup>
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
### Avatar
|
|
204
|
+
|
|
205
|
+
```tsx
|
|
206
|
+
import { Avatar } from "@synertech/ui";
|
|
207
|
+
|
|
208
|
+
<Avatar
|
|
209
|
+
src="/photo.jpg"
|
|
210
|
+
alt="Nom"
|
|
211
|
+
fallback="JD" // Initiales affichées si pas d'image
|
|
212
|
+
size="md" // "xs" | "sm" | "md" | "lg" | "xl"
|
|
213
|
+
variant="circle" // "circle" | "square" | "rounded"
|
|
214
|
+
border={false}
|
|
215
|
+
/>
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
### Skeleton
|
|
219
|
+
|
|
220
|
+
```tsx
|
|
221
|
+
import { Skeleton } from "@synertech/ui";
|
|
222
|
+
|
|
223
|
+
<Skeleton
|
|
224
|
+
variant="text" // "text" | "circular" | "rectangular" | "rounded"
|
|
225
|
+
animation="pulse" // "pulse" | "wave" | "none"
|
|
226
|
+
width={200}
|
|
227
|
+
height={20}
|
|
228
|
+
count={3} // Nombre de lignes
|
|
229
|
+
/>
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
### Indicator
|
|
233
|
+
|
|
234
|
+
```tsx
|
|
235
|
+
import { Indicator } from "@synertech/ui";
|
|
236
|
+
|
|
237
|
+
<Indicator
|
|
238
|
+
variant="count" // "dot" | "count" | "status" | "legend"
|
|
239
|
+
color="error" // "default" | "primary" | "success" | "warning" | "error" | "info" | "dark" | "purple" | "indigo" | "teal"
|
|
240
|
+
size="md" // "xs" | "sm" | "md" | "lg" | "xl"
|
|
241
|
+
placement="top-right"
|
|
242
|
+
count={5}
|
|
243
|
+
maxCount={99}
|
|
244
|
+
pulse={true}
|
|
245
|
+
bordered={true}
|
|
246
|
+
>
|
|
247
|
+
<Avatar src="/photo.jpg" />
|
|
248
|
+
</Indicator>
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
### Breadcrumb
|
|
252
|
+
|
|
253
|
+
```tsx
|
|
254
|
+
import { Breadcrumb } from "@synertech/ui";
|
|
255
|
+
|
|
256
|
+
<Breadcrumb
|
|
257
|
+
items={[
|
|
258
|
+
{ label: "Accueil", href: "/" },
|
|
259
|
+
{ label: "Produits", href: "/products" },
|
|
260
|
+
{ label: "Détail", icon: <ProductIcon /> },
|
|
261
|
+
]}
|
|
262
|
+
separator="/"
|
|
263
|
+
size="md" // "sm" | "md" | "lg"
|
|
264
|
+
maxItems={4}
|
|
265
|
+
/>
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
### Pagination
|
|
269
|
+
|
|
270
|
+
```tsx
|
|
271
|
+
import { Pagination } from "@synertech/ui";
|
|
272
|
+
|
|
273
|
+
<Pagination
|
|
274
|
+
page={currentPage}
|
|
275
|
+
totalPages={10}
|
|
276
|
+
onPageChange={setCurrentPage}
|
|
277
|
+
size="md" // "sm" | "md" | "lg"
|
|
278
|
+
variant="default" // "default" | "outlined" | "minimal"
|
|
279
|
+
showFirstLast={true}
|
|
280
|
+
siblings={1}
|
|
281
|
+
totalItems={100}
|
|
282
|
+
pageSize={10}
|
|
283
|
+
pageSizeOptions={[10, 20, 50]}
|
|
284
|
+
onPageSizeChange={setPageSize}
|
|
285
|
+
/>
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
### Dropdown (Composable)
|
|
289
|
+
|
|
290
|
+
```tsx
|
|
291
|
+
import {
|
|
292
|
+
Dropdown, DropdownTrigger, DropdownContent,
|
|
293
|
+
DropdownItem, DropdownCheckboxItem, DropdownRadio,
|
|
294
|
+
DropdownSeparator, DropdownSubMenu,
|
|
295
|
+
} from "@synertech/ui";
|
|
296
|
+
|
|
297
|
+
<Dropdown
|
|
298
|
+
defaultOpen={false}
|
|
299
|
+
closeOnSelect={true}
|
|
300
|
+
onOpenChange={(open) => console.log(open)}
|
|
301
|
+
>
|
|
302
|
+
<DropdownTrigger asChild>
|
|
303
|
+
<Button>Menu</Button>
|
|
304
|
+
</DropdownTrigger>
|
|
305
|
+
|
|
306
|
+
<DropdownContent
|
|
307
|
+
side="bottom" // "top" | "right" | "bottom" | "left"
|
|
308
|
+
align="start" // "start" | "center" | "end"
|
|
309
|
+
sideOffset={4}
|
|
310
|
+
animation="scale" // "fade" | "scale" | "slide" | "none"
|
|
311
|
+
maxHeight={300}
|
|
312
|
+
>
|
|
313
|
+
<DropdownItem icon={<EditIcon />} shortcut="Ctrl+E" onClick={handleEdit}>
|
|
314
|
+
Modifier
|
|
315
|
+
</DropdownItem>
|
|
316
|
+
<DropdownItem variant="danger" onClick={handleDelete}>
|
|
317
|
+
Supprimer
|
|
318
|
+
</DropdownItem>
|
|
319
|
+
|
|
320
|
+
<DropdownSeparator />
|
|
321
|
+
|
|
322
|
+
<DropdownCheckboxItem
|
|
323
|
+
checked={isVisible}
|
|
324
|
+
onCheckedChange={setVisible}
|
|
325
|
+
>
|
|
326
|
+
Afficher les détails
|
|
327
|
+
</DropdownCheckboxItem>
|
|
328
|
+
|
|
329
|
+
<DropdownSubMenu trigger="Plus d'options">
|
|
330
|
+
<DropdownItem>Exporter</DropdownItem>
|
|
331
|
+
<DropdownItem>Archiver</DropdownItem>
|
|
332
|
+
</DropdownSubMenu>
|
|
333
|
+
</DropdownContent>
|
|
334
|
+
</Dropdown>
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
### Tabs (Composable)
|
|
338
|
+
|
|
339
|
+
```tsx
|
|
340
|
+
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@synertech/ui";
|
|
341
|
+
|
|
342
|
+
<Tabs
|
|
343
|
+
defaultValue="tab1"
|
|
344
|
+
variant="underline" // "underline" | "pills" | "enclosed" | "soft"
|
|
345
|
+
size="md" // "sm" | "md" | "lg"
|
|
346
|
+
color="primary"
|
|
347
|
+
onValueChange={(v) => console.log(v)}
|
|
348
|
+
>
|
|
349
|
+
<TabsList>
|
|
350
|
+
<TabsTrigger value="tab1">Onglet 1</TabsTrigger>
|
|
351
|
+
<TabsTrigger value="tab2">Onglet 2</TabsTrigger>
|
|
352
|
+
</TabsList>
|
|
353
|
+
<TabsContent value="tab1">Contenu 1</TabsContent>
|
|
354
|
+
<TabsContent value="tab2">Contenu 2</TabsContent>
|
|
355
|
+
</Tabs>
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
### Input (Form)
|
|
359
|
+
|
|
360
|
+
```tsx
|
|
361
|
+
import { Input } from "@synertech/ui";
|
|
362
|
+
|
|
363
|
+
<Input
|
|
364
|
+
label="Email"
|
|
365
|
+
type="email"
|
|
366
|
+
placeholder="you@example.com"
|
|
367
|
+
helperText="Votre adresse email professionnelle"
|
|
368
|
+
error={false}
|
|
369
|
+
hideLabel={false}
|
|
370
|
+
inline={false}
|
|
371
|
+
multiline={false} // true pour textarea
|
|
372
|
+
rows={3} // Nombre de lignes si multiline
|
|
373
|
+
focused={false} // Focus programmatique
|
|
374
|
+
/>
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
### Select / Combobox
|
|
378
|
+
|
|
379
|
+
```tsx
|
|
380
|
+
import { Select } from "@synertech/ui";
|
|
381
|
+
import type { SelectOption } from "@synertech/ui";
|
|
382
|
+
|
|
383
|
+
const options: SelectOption[] = [
|
|
384
|
+
{ value: "fr", label: "France", icon: <FrFlag /> },
|
|
385
|
+
{ value: "ca", label: "Canada", description: "Amérique du Nord" },
|
|
386
|
+
{ value: "jp", label: "Japon", keywords: ["japan", "nippon"] },
|
|
387
|
+
];
|
|
388
|
+
|
|
389
|
+
<Select
|
|
390
|
+
options={options}
|
|
391
|
+
value={country}
|
|
392
|
+
onValueChange={setCountry}
|
|
393
|
+
searchable={true}
|
|
394
|
+
clearable={true}
|
|
395
|
+
loading={false}
|
|
396
|
+
size="md" // "sm" | "md" | "lg"
|
|
397
|
+
variant="default" // "default" | "outlined" | "filled"
|
|
398
|
+
placeholder="Choisir un pays..."
|
|
399
|
+
label="Pays"
|
|
400
|
+
description="Sélectionnez votre pays"
|
|
401
|
+
helperText="Requis pour la facturation"
|
|
402
|
+
error="Ce champ est requis"
|
|
403
|
+
required={true}
|
|
404
|
+
noOptionsText="Aucun résultat"
|
|
405
|
+
maxHeight={256}
|
|
406
|
+
name="country" // Pour les formulaires natifs
|
|
407
|
+
/>
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
### Checkbox
|
|
411
|
+
|
|
412
|
+
```tsx
|
|
413
|
+
import { Checkbox } from "@synertech/ui";
|
|
414
|
+
|
|
415
|
+
<Checkbox
|
|
416
|
+
checked={accepted}
|
|
417
|
+
onChange={(checked) => setAccepted(checked)}
|
|
418
|
+
label="J'accepte les conditions"
|
|
419
|
+
description="En cochant, vous acceptez nos CGU"
|
|
420
|
+
error="Vous devez accepter les conditions"
|
|
421
|
+
size="md" // "sm" | "md" | "lg"
|
|
422
|
+
variant="default" // "default" | "filled" | "outlined"
|
|
423
|
+
shape="square" // "square" | "rounded" | "circle"
|
|
424
|
+
labelPosition="right" // "left" | "right"
|
|
425
|
+
indeterminate={false}
|
|
426
|
+
checkedColor="bg-blue-600"
|
|
427
|
+
/>
|
|
428
|
+
```
|
|
429
|
+
|
|
430
|
+
### RadioButton
|
|
431
|
+
|
|
432
|
+
```tsx
|
|
433
|
+
import { RadioButton } from "@synertech/ui";
|
|
434
|
+
|
|
435
|
+
{["monthly", "yearly"].map((plan) => (
|
|
436
|
+
<RadioButton
|
|
437
|
+
key={plan}
|
|
438
|
+
value={plan}
|
|
439
|
+
checked={selectedPlan === plan}
|
|
440
|
+
onCheckedChange={() => setSelectedPlan(plan)}
|
|
441
|
+
label={plan === "monthly" ? "Mensuel" : "Annuel"}
|
|
442
|
+
description="Facturé chaque mois"
|
|
443
|
+
size="md" // "sm" | "md" | "lg"
|
|
444
|
+
variant="default" // "default" | "filled" | "outlined"
|
|
445
|
+
/>
|
|
446
|
+
))}
|
|
447
|
+
```
|
|
448
|
+
|
|
449
|
+
### Slider
|
|
450
|
+
|
|
451
|
+
```tsx
|
|
452
|
+
import { Slider } from "@synertech/ui";
|
|
453
|
+
import type { SliderMark } from "@synertech/ui";
|
|
454
|
+
|
|
455
|
+
<Slider
|
|
456
|
+
value={volume} // controlled (number)
|
|
457
|
+
onValueChange={setVolume} // (value: number) => void
|
|
458
|
+
// defaultValue={40} // alternative non-contrôlée
|
|
459
|
+
min={0}
|
|
460
|
+
max={100}
|
|
461
|
+
step={1}
|
|
462
|
+
size="md" // "sm" | "md" | "lg"
|
|
463
|
+
color="primary" // "primary" | "success" | "warning" | "error" | "info" | "neutral"
|
|
464
|
+
label="Volume"
|
|
465
|
+
description="Réglez le niveau sonore"
|
|
466
|
+
showValue={true} // affiche la valeur à côté du label
|
|
467
|
+
showTooltip={true} // bulle de valeur au survol/focus du curseur
|
|
468
|
+
valueFormat={(v) => `${v} %`} // formatage de la valeur affichée
|
|
469
|
+
error="Valeur trop élevée" // boolean | string
|
|
470
|
+
disabled={false}
|
|
471
|
+
readOnly={false}
|
|
472
|
+
/>
|
|
473
|
+
|
|
474
|
+
// Marques (graduations) — tableau explicite…
|
|
475
|
+
const marks: SliderMark[] = [
|
|
476
|
+
{ value: 0, label: "0%" },
|
|
477
|
+
{ value: 50, label: "50%" },
|
|
478
|
+
{ value: 100, label: "100%" },
|
|
479
|
+
];
|
|
480
|
+
<Slider min={0} max={100} step={50} marks={marks} defaultValue={50} />
|
|
481
|
+
|
|
482
|
+
// …ou `marks` auto-générées à chaque step
|
|
483
|
+
<Slider min={0} max={10} step={1} marks defaultValue={6} showValue />
|
|
484
|
+
|
|
485
|
+
// Couleurs personnalisées (au lieu de `color`)
|
|
486
|
+
<Slider
|
|
487
|
+
defaultValue={55}
|
|
488
|
+
trackColor="#fde68a" // rail
|
|
489
|
+
fillColor="#d97706" // portion remplie
|
|
490
|
+
thumbColor="#92400e" // curseur
|
|
491
|
+
/>
|
|
492
|
+
```
|
|
493
|
+
|
|
494
|
+
> Construit sur un `<input type="range">` natif (clavier + ARIA), avec rail/remplissage/curseur stylés. Le curseur visible et le remplissage sont décalés de la largeur du curseur pour rester alignés et ne jamais déborder du rail.
|
|
495
|
+
|
|
496
|
+
### ImageUploader
|
|
497
|
+
|
|
498
|
+
```tsx
|
|
499
|
+
import { ImageUploader } from "@synertech/ui";
|
|
500
|
+
|
|
501
|
+
<ImageUploader
|
|
502
|
+
value={imageUrl}
|
|
503
|
+
onChange={setImageUrl}
|
|
504
|
+
onUpload={async (file) => {
|
|
505
|
+
const url = await uploadToS3(file);
|
|
506
|
+
return url;
|
|
507
|
+
}}
|
|
508
|
+
label="Photo de profil"
|
|
509
|
+
maxSize={5 * 1024 * 1024} // 5MB
|
|
510
|
+
accept="image/*"
|
|
511
|
+
showUrlInput={true}
|
|
512
|
+
aspectRatio="1/1" // "16/9" | "4/3" | "1/1" | "auto"
|
|
513
|
+
/>
|
|
514
|
+
```
|
|
515
|
+
|
|
516
|
+
### Card (Composable)
|
|
517
|
+
|
|
518
|
+
```tsx
|
|
519
|
+
import { Card, CardHeader, CardBody, CardFooter, CardImage, CardTitle, CardDescription } from "@synertech/ui";
|
|
520
|
+
|
|
521
|
+
<Card
|
|
522
|
+
variant="outlined" // "default" | "outlined" | "elevated" | "filled"
|
|
523
|
+
rounded="lg" // "none" | "sm" | "md" | "lg" | "xl" | "full"
|
|
524
|
+
padding="none" // "none" | "sm" | "md" | "lg"
|
|
525
|
+
hoverable={true}
|
|
526
|
+
clickable={false}
|
|
527
|
+
as="article" // "article" | "section" | "div"
|
|
528
|
+
>
|
|
529
|
+
<CardImage src="/cover.jpg" alt="Cover" />
|
|
530
|
+
<CardHeader actions={<Button size="sm">Edit</Button>} divider>
|
|
531
|
+
<CardTitle>Titre de la carte</CardTitle>
|
|
532
|
+
<CardDescription>Description courte</CardDescription>
|
|
533
|
+
</CardHeader>
|
|
534
|
+
<CardBody>
|
|
535
|
+
<p>Contenu principal ici</p>
|
|
536
|
+
</CardBody>
|
|
537
|
+
<CardFooter>
|
|
538
|
+
<Button>Action</Button>
|
|
539
|
+
</CardFooter>
|
|
540
|
+
</Card>
|
|
541
|
+
```
|
|
542
|
+
|
|
543
|
+
### Table (Composable)
|
|
544
|
+
|
|
545
|
+
```tsx
|
|
546
|
+
import { Table, TableHeader, TableHeaderCell, TableBody, TableRow, TableCell } from "@synertech/ui";
|
|
547
|
+
|
|
548
|
+
<Table>
|
|
549
|
+
<TableHeader>
|
|
550
|
+
<TableRow>
|
|
551
|
+
<TableHeaderCell>Nom</TableHeaderCell>
|
|
552
|
+
<TableHeaderCell>Email</TableHeaderCell>
|
|
553
|
+
<TableHeaderCell>Role</TableHeaderCell>
|
|
554
|
+
</TableRow>
|
|
555
|
+
</TableHeader>
|
|
556
|
+
<TableBody>
|
|
557
|
+
{users.map((user) => (
|
|
558
|
+
<TableRow key={user.id}>
|
|
559
|
+
<TableCell>{user.name}</TableCell>
|
|
560
|
+
<TableCell>{user.email}</TableCell>
|
|
561
|
+
<TableCell><Chip label={user.role} size="sm" /></TableCell>
|
|
562
|
+
</TableRow>
|
|
563
|
+
))}
|
|
564
|
+
</TableBody>
|
|
565
|
+
</Table>
|
|
566
|
+
```
|
|
567
|
+
|
|
568
|
+
### Chip
|
|
569
|
+
|
|
570
|
+
```tsx
|
|
571
|
+
import { Chip } from "@synertech/ui";
|
|
572
|
+
|
|
573
|
+
<Chip
|
|
574
|
+
label="Active"
|
|
575
|
+
variant="soft" // "solid" | "outlined" | "soft" | "dot"
|
|
576
|
+
size="md" // "sm" | "md" | "lg"
|
|
577
|
+
color="success" // "default" | "primary" | "success" | "warning" | "error" | "info"
|
|
578
|
+
rounded="full" // "none" | "sm" | "md" | "lg" | "full"
|
|
579
|
+
icon={<CheckIcon />}
|
|
580
|
+
closable={true}
|
|
581
|
+
onClose={handleRemove}
|
|
582
|
+
clickable={true}
|
|
583
|
+
selected={false}
|
|
584
|
+
as="button" // "span" | "div" | "button"
|
|
585
|
+
/>
|
|
586
|
+
```
|
|
587
|
+
|
|
588
|
+
### Label
|
|
589
|
+
|
|
590
|
+
```tsx
|
|
591
|
+
import { Label } from "@synertech/ui";
|
|
592
|
+
|
|
593
|
+
<Label
|
|
594
|
+
htmlFor="email"
|
|
595
|
+
size="md" // "xs" | "sm" | "md" | "lg" | "xl"
|
|
596
|
+
color="default" // "default" | "primary" | "secondary" | "success" | "warning" | "error" | "info" | "muted"
|
|
597
|
+
weight="medium" // "normal" | "medium" | "semibold" | "bold"
|
|
598
|
+
required={true}
|
|
599
|
+
tooltip="Information utile"
|
|
600
|
+
icon={<MailIcon />}
|
|
601
|
+
as="label" // "label" | "span" | "p"
|
|
602
|
+
>
|
|
603
|
+
Adresse email
|
|
604
|
+
</Label>
|
|
605
|
+
```
|
|
606
|
+
|
|
607
|
+
### Banner
|
|
608
|
+
|
|
609
|
+
```tsx
|
|
610
|
+
import { Banner } from "@synertech/ui";
|
|
611
|
+
|
|
612
|
+
<Banner
|
|
613
|
+
variant="notification" // "notification" | "hero" | "promotion"
|
|
614
|
+
type="warning" // "info" | "success" | "warning" | "error" | "default"
|
|
615
|
+
title="Maintenance prévue"
|
|
616
|
+
description="Le système sera indisponible demain de 2h à 4h."
|
|
617
|
+
icon={<WarningIcon />}
|
|
618
|
+
closable={true}
|
|
619
|
+
onClose={handleDismiss}
|
|
620
|
+
action={<Button size="sm">En savoir plus</Button>}
|
|
621
|
+
/>
|
|
622
|
+
```
|
|
623
|
+
|
|
624
|
+
### Header
|
|
625
|
+
|
|
626
|
+
```tsx
|
|
627
|
+
import { Header } from "@synertech/ui";
|
|
628
|
+
|
|
629
|
+
<Header
|
|
630
|
+
logo={<img src="/logo.svg" alt="Logo" />}
|
|
631
|
+
logoHref="/"
|
|
632
|
+
navItems={[
|
|
633
|
+
{ label: "Accueil", href: "/" },
|
|
634
|
+
{ label: "Produits", href: "/products", badge: "Nouveau" },
|
|
635
|
+
{ label: "Docs", href: "/docs", external: true },
|
|
636
|
+
]}
|
|
637
|
+
actions={<Button size="sm">Connexion</Button>}
|
|
638
|
+
variant="bordered" // "default" | "transparent" | "bordered" | "shadow"
|
|
639
|
+
size="md" // "sm" | "md" | "lg"
|
|
640
|
+
sticky={true}
|
|
641
|
+
showSearch={true}
|
|
642
|
+
onSearch={(q) => router.push(`/search?q=${q}`)}
|
|
643
|
+
/>
|
|
644
|
+
```
|
|
645
|
+
|
|
646
|
+
### Footer
|
|
647
|
+
|
|
648
|
+
```tsx
|
|
649
|
+
import { Footer } from "@synertech/ui";
|
|
650
|
+
|
|
651
|
+
<Footer
|
|
652
|
+
variant="bordered" // "default" | "bordered" | "minimal" | "dark"
|
|
653
|
+
size="md" // "sm" | "md" | "lg"
|
|
654
|
+
sticky={false}
|
|
655
|
+
>
|
|
656
|
+
<p>2024 Synertech. Tous droits reserves.</p>
|
|
657
|
+
</Footer>
|
|
658
|
+
```
|
|
659
|
+
|
|
660
|
+
### Drawer
|
|
661
|
+
|
|
662
|
+
```tsx
|
|
663
|
+
import { Drawer } from "@synertech/ui";
|
|
664
|
+
|
|
665
|
+
<Drawer
|
|
666
|
+
open={isDrawerOpen}
|
|
667
|
+
onClose={() => setDrawerOpen(false)}
|
|
668
|
+
position="right" // "left" | "right" | "top" | "bottom"
|
|
669
|
+
size="md" // "sm" | "md" | "lg" | "xl" | "full"
|
|
670
|
+
title="Filtres"
|
|
671
|
+
footer={<Button onClick={applyFilters}>Appliquer</Button>}
|
|
672
|
+
showOverlay={true}
|
|
673
|
+
closeOnOverlayClick={true}
|
|
674
|
+
closeOnEscape={true}
|
|
675
|
+
// Accessibilite (voir la section "Dialogues accessibles")
|
|
676
|
+
closeLabel="Fermer le panneau" // nom accessible du bouton de fermeture
|
|
677
|
+
aria-label="Menu principal" // nom du dialogue si aucun `title`
|
|
678
|
+
trapFocus={true} // Tab / Maj+Tab confines dans le drawer
|
|
679
|
+
initialFocus="dialog" // "dialog" | "first" | "none" | ref
|
|
680
|
+
returnFocus={true} // true | false | ref
|
|
681
|
+
lockScroll={true}
|
|
682
|
+
>
|
|
683
|
+
<p>Contenu du panneau</p>
|
|
684
|
+
</Drawer>
|
|
685
|
+
```
|
|
686
|
+
|
|
687
|
+
Le Drawer est un vrai dialogue modal : `role="dialog"` + `aria-modal`, focus
|
|
688
|
+
deplace a l'ouverture, Tab confine, Echap, retour du focus au declencheur et
|
|
689
|
+
reste de la page `inert`. L'overlay reste cliquable.
|
|
690
|
+
|
|
691
|
+
### Divider
|
|
692
|
+
|
|
693
|
+
```tsx
|
|
694
|
+
import { Divider } from "@synertech/ui";
|
|
695
|
+
|
|
696
|
+
<Divider
|
|
697
|
+
orientation="horizontal" // "horizontal" | "vertical"
|
|
698
|
+
variant="solid" // "solid" | "dashed" | "dotted"
|
|
699
|
+
label="ou"
|
|
700
|
+
textAlign="center" // "left" | "center" | "right"
|
|
701
|
+
thickness={1}
|
|
702
|
+
/>
|
|
703
|
+
```
|
|
704
|
+
|
|
705
|
+
### Modal
|
|
706
|
+
|
|
707
|
+
```tsx
|
|
708
|
+
import { Modal } from "@synertech/ui";
|
|
709
|
+
|
|
710
|
+
<Modal
|
|
711
|
+
isOpen={isModalOpen}
|
|
712
|
+
onClose={() => setModalOpen(false)}
|
|
713
|
+
title="Confirmer la suppression"
|
|
714
|
+
description="Cette action est irreversible."
|
|
715
|
+
icon={<TrashIcon />}
|
|
716
|
+
size="md" // "sm" | "md" | "lg" | "xl" | "full"
|
|
717
|
+
showSubmit={true}
|
|
718
|
+
submitLabel="Supprimer"
|
|
719
|
+
onSubmitClick={handleDelete}
|
|
720
|
+
showCancel={true}
|
|
721
|
+
cancelLabel="Annuler"
|
|
722
|
+
buttonsPosition="right" // "left" | "center" | "right"
|
|
723
|
+
reverseButtons={false}
|
|
724
|
+
allowFullScreen={false}
|
|
725
|
+
showHeaderDivider={true}
|
|
726
|
+
showFooterDivider={true}
|
|
727
|
+
// Accessibilite (voir la section "Dialogues accessibles")
|
|
728
|
+
closeLabel="Fermer la fenetre" // nom accessible du bouton de fermeture
|
|
729
|
+
enterFullScreenLabel="Agrandir"
|
|
730
|
+
exitFullScreenLabel="Reduire"
|
|
731
|
+
aria-label="Detail de la tache" // nom du dialogue si `title=""`
|
|
732
|
+
closeOnEscape={true}
|
|
733
|
+
trapFocus={true} // Tab / Maj+Tab confines dans la modale
|
|
734
|
+
initialFocus="dialog" // "dialog" | "first" | "none" | ref
|
|
735
|
+
returnFocus={true} // true | false | ref
|
|
736
|
+
lockScroll={true}
|
|
737
|
+
>
|
|
738
|
+
<p>Etes-vous sur de vouloir supprimer cet element ?</p>
|
|
739
|
+
</Modal>
|
|
740
|
+
```
|
|
741
|
+
|
|
742
|
+
La modale est un vrai dialogue : `role="dialog"` + `aria-modal="true"`, nommee
|
|
743
|
+
par son `title` (`aria-labelledby`) et decrite par sa `description`
|
|
744
|
+
(`aria-describedby`). Elle prend le focus a l'ouverture, confine Tab / Maj+Tab,
|
|
745
|
+
se ferme avec Echap, rend le focus au declencheur et rend le reste de la page
|
|
746
|
+
`inert`. Les modales imbriquees sont empilees : Echap ne ferme que celle du
|
|
747
|
+
dessus.
|
|
748
|
+
|
|
749
|
+
**Ne pas** reimplementer un focus trap local ni ajouter `role="dialog"` sur un
|
|
750
|
+
enfant : tout est deja fourni par le composant.
|
|
751
|
+
|
|
752
|
+
### Tooltip
|
|
753
|
+
|
|
754
|
+
```tsx
|
|
755
|
+
import { Tooltip } from "@synertech/ui";
|
|
756
|
+
|
|
757
|
+
<Tooltip
|
|
758
|
+
content="Information utile"
|
|
759
|
+
position="top" // "top" | "bottom" | "left" | "right"
|
|
760
|
+
variant="default" // "default" | "light" | "error" | "success" | "warning" | "info"
|
|
761
|
+
size="md" // "sm" | "md" | "lg"
|
|
762
|
+
trigger="hover" // "hover" | "click" | "manual"
|
|
763
|
+
delay={0}
|
|
764
|
+
showArrow={true}
|
|
765
|
+
>
|
|
766
|
+
<Button>Survolez-moi</Button>
|
|
767
|
+
</Tooltip>
|
|
768
|
+
```
|
|
769
|
+
|
|
770
|
+
### Toast
|
|
771
|
+
|
|
772
|
+
```tsx
|
|
773
|
+
import { Toast } from "@synertech/ui";
|
|
774
|
+
|
|
775
|
+
<Toast
|
|
776
|
+
title="Sauvegarde reussie"
|
|
777
|
+
description="Vos modifications ont ete enregistrees."
|
|
778
|
+
type="success" // "success" | "error" | "warning" | "info" | "default"
|
|
779
|
+
variant="soft" // "solid" | "outlined" | "soft"
|
|
780
|
+
position="top-right"
|
|
781
|
+
closable={true}
|
|
782
|
+
duration={5000}
|
|
783
|
+
onClose={handleDismiss}
|
|
784
|
+
action={<Button size="sm">Annuler</Button>}
|
|
785
|
+
/>
|
|
786
|
+
```
|
|
787
|
+
|
|
788
|
+
### CircularProgress
|
|
789
|
+
|
|
790
|
+
```tsx
|
|
791
|
+
import { CircularProgress } from "@synertech/ui";
|
|
792
|
+
|
|
793
|
+
// Indéterminé (spinner)
|
|
794
|
+
<CircularProgress indeterminate size="md" color="primary" />
|
|
795
|
+
|
|
796
|
+
// Déterminé (progression)
|
|
797
|
+
<CircularProgress
|
|
798
|
+
value={75}
|
|
799
|
+
size="lg" // "xs" | "sm" | "md" | "lg" | "xl"
|
|
800
|
+
thickness="normal" // "thin" | "normal" | "thick"
|
|
801
|
+
color="success" // "default" | "primary" | "success" | "warning" | "error" | "info"
|
|
802
|
+
showValue={true}
|
|
803
|
+
animated={true}
|
|
804
|
+
/>
|
|
805
|
+
```
|
|
806
|
+
|
|
807
|
+
### HelpButton
|
|
808
|
+
|
|
809
|
+
```tsx
|
|
810
|
+
import { HelpButton, WithHelpButton } from "@synertech/ui";
|
|
811
|
+
|
|
812
|
+
// Standalone
|
|
813
|
+
<HelpButton
|
|
814
|
+
content="Explication en **Markdown** supportee"
|
|
815
|
+
markdown={true}
|
|
816
|
+
position="bottom" // "top" | "bottom" | "left" | "right"
|
|
817
|
+
size="sm" // "xs" | "sm" | "md" | "lg"
|
|
818
|
+
width="md" // "sm" | "md" | "lg" | "xl"
|
|
819
|
+
editable={true}
|
|
820
|
+
onSave={async (content) => { await save(content); return true; }}
|
|
821
|
+
title="Aide"
|
|
822
|
+
/>
|
|
823
|
+
|
|
824
|
+
// En wrapper autour d'un autre element
|
|
825
|
+
<WithHelpButton content="Explication" position="right">
|
|
826
|
+
<Input label="Champ complexe" />
|
|
827
|
+
</WithHelpButton>
|
|
828
|
+
```
|
|
829
|
+
|
|
830
|
+
### Detachable
|
|
831
|
+
|
|
832
|
+
```tsx
|
|
833
|
+
import { Detachable } from "@synertech/ui";
|
|
834
|
+
import { useRef } from "react";
|
|
835
|
+
|
|
836
|
+
const ref = useRef<DetachableRef>(null);
|
|
837
|
+
|
|
838
|
+
<Detachable
|
|
839
|
+
ref={ref}
|
|
840
|
+
title="Editeur detache"
|
|
841
|
+
width={800}
|
|
842
|
+
height={600}
|
|
843
|
+
center={true}
|
|
844
|
+
copyStyles={true}
|
|
845
|
+
onDetach={() => console.log("Detached")}
|
|
846
|
+
onClose={() => console.log("Re-attached")}
|
|
847
|
+
onPopupBlocked={() => alert("Popup bloque")}
|
|
848
|
+
>
|
|
849
|
+
<CodeEditor value={code} onChange={setCode} />
|
|
850
|
+
</Detachable>
|
|
851
|
+
|
|
852
|
+
// API imperative
|
|
853
|
+
<Button onClick={() => ref.current?.detach()}>Detacher</Button>
|
|
854
|
+
<Button onClick={() => ref.current?.attach()}>Rattacher</Button>
|
|
855
|
+
```
|
|
856
|
+
|
|
857
|
+
### AdvanceDetachable
|
|
858
|
+
|
|
859
|
+
```tsx
|
|
860
|
+
import { AdvanceDetachable } from "@synertech/ui";
|
|
861
|
+
import { useRef } from "react";
|
|
862
|
+
|
|
863
|
+
const ref = useRef<AdvanceDetachableRef>(null);
|
|
864
|
+
|
|
865
|
+
// Mode "detach" : deplace le contenu dans la popup
|
|
866
|
+
<AdvanceDetachable
|
|
867
|
+
ref={ref}
|
|
868
|
+
id="editor-1"
|
|
869
|
+
mode="detach" // "detach" | "clone"
|
|
870
|
+
title="Editeur"
|
|
871
|
+
value={formData}
|
|
872
|
+
onValueChange={setFormData}
|
|
873
|
+
>
|
|
874
|
+
{({ open, close, toggle, detached }) => (
|
|
875
|
+
<div>
|
|
876
|
+
<Button onClick={toggle}>{detached ? "Rattacher" : "Detacher"}</Button>
|
|
877
|
+
<FormEditor data={formData} onChange={setFormData} />
|
|
878
|
+
</div>
|
|
879
|
+
)}
|
|
880
|
+
</AdvanceDetachable>
|
|
881
|
+
|
|
882
|
+
// Mode "clone" : synchronise via BroadcastChannel
|
|
883
|
+
<AdvanceDetachable
|
|
884
|
+
id="sync-editor"
|
|
885
|
+
mode="clone"
|
|
886
|
+
value={data}
|
|
887
|
+
onValueChange={setData}
|
|
888
|
+
serialize={JSON.stringify}
|
|
889
|
+
deserialize={JSON.parse}
|
|
890
|
+
>
|
|
891
|
+
<DataEditor />
|
|
892
|
+
</AdvanceDetachable>
|
|
893
|
+
```
|
|
894
|
+
|
|
895
|
+
### WorkflowEditor
|
|
896
|
+
|
|
897
|
+
```tsx
|
|
898
|
+
import { WorkflowEditor } from "@synertech/ui";
|
|
899
|
+
import type { NodeRegistry, WorkflowValue, WorkflowEditorRef } from "@synertech/ui";
|
|
900
|
+
import { useRef } from "react";
|
|
901
|
+
|
|
902
|
+
const ref = useRef<WorkflowEditorRef>(null);
|
|
903
|
+
|
|
904
|
+
const nodeRegistry: NodeRegistry = {
|
|
905
|
+
start: {
|
|
906
|
+
label: "Debut",
|
|
907
|
+
color: "#10b981",
|
|
908
|
+
ports: { outputs: [{ id: "out", label: "Suivant" }] },
|
|
909
|
+
render: (node) => <div>{node.data.label}</div>,
|
|
910
|
+
},
|
|
911
|
+
action: {
|
|
912
|
+
label: "Action",
|
|
913
|
+
color: "#3b82f6",
|
|
914
|
+
ports: {
|
|
915
|
+
inputs: [{ id: "in", label: "Entree" }],
|
|
916
|
+
outputs: [{ id: "out", label: "Sortie" }],
|
|
917
|
+
},
|
|
918
|
+
},
|
|
919
|
+
};
|
|
920
|
+
|
|
921
|
+
<WorkflowEditor
|
|
922
|
+
ref={ref}
|
|
923
|
+
defaultValue={{ nodes: [], edges: [] }}
|
|
924
|
+
onValueChange={(val) => console.log(val)}
|
|
925
|
+
nodeRegistry={nodeRegistry}
|
|
926
|
+
direction="horizontal" // "horizontal" | "vertical"
|
|
927
|
+
draggableNodes={true}
|
|
928
|
+
connectable={true}
|
|
929
|
+
selectable={true}
|
|
930
|
+
panOnDrag={true}
|
|
931
|
+
zoomOnWheel={true}
|
|
932
|
+
snapToGrid={false}
|
|
933
|
+
gridSize={20}
|
|
934
|
+
showGrid={true}
|
|
935
|
+
toolbar={true}
|
|
936
|
+
autoFit={false}
|
|
937
|
+
minZoom={0.1}
|
|
938
|
+
maxZoom={3}
|
|
939
|
+
/>
|
|
940
|
+
|
|
941
|
+
// API imperative
|
|
942
|
+
ref.current?.fitView();
|
|
943
|
+
ref.current?.resetViewport();
|
|
944
|
+
ref.current?.deleteNode("node-1");
|
|
945
|
+
```
|
|
946
|
+
|
|
947
|
+
### FileSelector
|
|
948
|
+
|
|
949
|
+
```tsx
|
|
950
|
+
import { FileSelector } from "@synertech/ui";
|
|
951
|
+
|
|
952
|
+
// Sélection de fichiers (défaut)
|
|
953
|
+
<FileSelector
|
|
954
|
+
mode="file" // "file" | "directory" | "both"
|
|
955
|
+
variant="dropzone" // "dropzone" | "button"
|
|
956
|
+
size="md" // "sm" | "md" | "lg"
|
|
957
|
+
accept="image/*,.pdf"
|
|
958
|
+
multiple={true}
|
|
959
|
+
maxFiles={5}
|
|
960
|
+
maxSize={10 * 1024 * 1024} // 10MB
|
|
961
|
+
value={files}
|
|
962
|
+
onChange={setFiles}
|
|
963
|
+
onReject={(file, reason) => console.log(reason)}
|
|
964
|
+
label="Upload documents"
|
|
965
|
+
helperText="PDF ou images, max 10MB"
|
|
966
|
+
error="Ce champ est requis"
|
|
967
|
+
showFileList={true}
|
|
968
|
+
placeholder="Glissez des fichiers ici, ou"
|
|
969
|
+
browseLabel="Parcourir"
|
|
970
|
+
disabled={false}
|
|
971
|
+
readOnly={false}
|
|
972
|
+
/>
|
|
973
|
+
|
|
974
|
+
// Sélection de dossiers
|
|
975
|
+
<FileSelector
|
|
976
|
+
mode="directory"
|
|
977
|
+
label="Sélectionner un dossier"
|
|
978
|
+
multiple={true}
|
|
979
|
+
onChange={(files) => {
|
|
980
|
+
// files contient tous les fichiers du dossier sélectionné
|
|
981
|
+
// file.webkitRelativePath contient le chemin relatif
|
|
982
|
+
console.log(files);
|
|
983
|
+
}}
|
|
984
|
+
/>
|
|
985
|
+
|
|
986
|
+
// Les deux : fichiers ou dossiers
|
|
987
|
+
<FileSelector
|
|
988
|
+
mode="both"
|
|
989
|
+
label="Fichiers ou dossiers"
|
|
990
|
+
browseLabel="Parcourir fichiers"
|
|
991
|
+
browseFolderLabel="Parcourir dossier"
|
|
992
|
+
multiple={true}
|
|
993
|
+
onChange={setFiles}
|
|
994
|
+
/>
|
|
995
|
+
```
|
|
996
|
+
|
|
997
|
+
**Notes :**
|
|
998
|
+
- En mode `"both"` dropzone, deux boutons "Browse files" et "Browse folder" sont affichés
|
|
999
|
+
- En mode `"both"` button variant, deux boutons côte à côte
|
|
1000
|
+
- Le drag & drop de dossiers traverse l'arborescence récursivement
|
|
1001
|
+
- `webkitRelativePath` est affiché dans la liste de fichiers quand disponible
|
|
1002
|
+
- `accept` n'est pas appliqué au dialogue natif en mode directory (navigateur l'ignore) mais la validation JS reste active
|
|
1003
|
+
|
|
1004
|
+
### DataGrid
|
|
1005
|
+
|
|
1006
|
+
```tsx
|
|
1007
|
+
import { DataGrid } from "@synertech/ui";
|
|
1008
|
+
import type { ColumnDef, SortState, DataGridRef } from "@synertech/ui";
|
|
1009
|
+
import { useRef } from "react";
|
|
1010
|
+
|
|
1011
|
+
const ref = useRef<DataGridRef>(null);
|
|
1012
|
+
|
|
1013
|
+
interface User {
|
|
1014
|
+
id: string;
|
|
1015
|
+
name: string;
|
|
1016
|
+
email: string;
|
|
1017
|
+
role: string;
|
|
1018
|
+
age: number;
|
|
1019
|
+
status: string;
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
const columns: ColumnDef<User>[] = [
|
|
1023
|
+
{ key: "name", header: "Nom", sortable: true, filterable: true },
|
|
1024
|
+
{ key: "email", header: "Email", sticky: "left" },
|
|
1025
|
+
{ key: "role", header: "Rôle", filterable: true, filterType: "select",
|
|
1026
|
+
filterOptions: ["Admin", "User", "Manager"] },
|
|
1027
|
+
{ key: "age", header: "Âge", sortable: true, align: "right",
|
|
1028
|
+
aggregate: "avg" },
|
|
1029
|
+
{ key: "status", header: "Statut",
|
|
1030
|
+
render: (val) => <Chip label={String(val)} color={val === "Active" ? "success" : "error"} size="sm" /> },
|
|
1031
|
+
{ key: "actions", type: "actions",
|
|
1032
|
+
render: (_, row) => <Button size="sm" onClick={() => edit(row)}>Modifier</Button> },
|
|
1033
|
+
];
|
|
1034
|
+
|
|
1035
|
+
<DataGrid
|
|
1036
|
+
ref={ref}
|
|
1037
|
+
columns={columns}
|
|
1038
|
+
data={users}
|
|
1039
|
+
rowKey="id"
|
|
1040
|
+
mode="client" // "client" | "server"
|
|
1041
|
+
|
|
1042
|
+
// Tri
|
|
1043
|
+
sortable
|
|
1044
|
+
multiSort={false}
|
|
1045
|
+
defaultSort={[{ key: "name", direction: "asc" }]}
|
|
1046
|
+
onSort={(sort) => console.log(sort)}
|
|
1047
|
+
|
|
1048
|
+
// Pagination
|
|
1049
|
+
pagination={{ pageSize: 10, pageSizeOptions: [10, 25, 50], showTotalItems: true }}
|
|
1050
|
+
onPageChange={(page) => console.log(page)}
|
|
1051
|
+
|
|
1052
|
+
// Sélection
|
|
1053
|
+
selectable
|
|
1054
|
+
selectionMode="multiple" // "single" | "multiple"
|
|
1055
|
+
onSelectionChange={(keys) => console.log(keys)}
|
|
1056
|
+
|
|
1057
|
+
// Recherche & Filtres
|
|
1058
|
+
searchable
|
|
1059
|
+
searchPlaceholder="Rechercher..."
|
|
1060
|
+
onSearch={(q) => console.log(q)}
|
|
1061
|
+
|
|
1062
|
+
// Lignes expandables
|
|
1063
|
+
expandable
|
|
1064
|
+
expandedContent={(row) => <div>Détails de {row.name}</div>}
|
|
1065
|
+
|
|
1066
|
+
// Édition inline
|
|
1067
|
+
editable
|
|
1068
|
+
onCellEdit={(rowKey, colKey, value) => console.log(rowKey, colKey, value)}
|
|
1069
|
+
|
|
1070
|
+
// Réordonnement
|
|
1071
|
+
reorderable
|
|
1072
|
+
onReorder={(from, to) => console.log(from, to)}
|
|
1073
|
+
|
|
1074
|
+
// Visibilité des colonnes
|
|
1075
|
+
columnToggle
|
|
1076
|
+
|
|
1077
|
+
// Groupement
|
|
1078
|
+
groupBy="role"
|
|
1079
|
+
|
|
1080
|
+
// Export
|
|
1081
|
+
exportable
|
|
1082
|
+
exportFormats={["csv", "json"]}
|
|
1083
|
+
exportFilename="users-export"
|
|
1084
|
+
|
|
1085
|
+
// Footer agrégats
|
|
1086
|
+
showFooter
|
|
1087
|
+
|
|
1088
|
+
// Visuel
|
|
1089
|
+
density="default" // "compact" | "default" | "comfortable"
|
|
1090
|
+
striped
|
|
1091
|
+
bordered={false}
|
|
1092
|
+
stickyHeader
|
|
1093
|
+
maxHeight="600px"
|
|
1094
|
+
loading={false}
|
|
1095
|
+
loadingRows={5}
|
|
1096
|
+
emptyState={<p>Aucun utilisateur trouvé</p>}
|
|
1097
|
+
|
|
1098
|
+
// Événements
|
|
1099
|
+
onRowClick={(row, index) => console.log(row)}
|
|
1100
|
+
onRowDoubleClick={(row, index) => console.log(row)}
|
|
1101
|
+
|
|
1102
|
+
// Styles
|
|
1103
|
+
className="my-4"
|
|
1104
|
+
rowClassName={(row) => row.status === "Inactive" ? "opacity-50" : ""}
|
|
1105
|
+
/>
|
|
1106
|
+
|
|
1107
|
+
// API impérative
|
|
1108
|
+
ref.current?.resetFilters();
|
|
1109
|
+
ref.current?.resetSort();
|
|
1110
|
+
ref.current?.selectAll();
|
|
1111
|
+
ref.current?.deselectAll();
|
|
1112
|
+
ref.current?.exportData("csv");
|
|
1113
|
+
ref.current?.scrollToRow(50);
|
|
1114
|
+
ref.current?.getVisibleData();
|
|
1115
|
+
```
|
|
1116
|
+
|
|
1117
|
+
**Quand utiliser DataGrid vs Table :**
|
|
1118
|
+
- **Table** : tableaux simples, contrôle total du markup, server-compatible
|
|
1119
|
+
- **DataGrid** : tableaux data-driven avec tri, pagination, filtres, sélection, export
|
|
1120
|
+
|
|
1121
|
+
**Mode server :**
|
|
1122
|
+
En mode `"server"`, le DataGrid ne fait pas le tri/pagination/filtre côté client. Il délègue via les callbacks `onSort`, `onPageChange`, `onSearch`, `onFilterChange`. Utilisez `totalItems` pour la pagination serveur.
|
|
1123
|
+
|
|
1124
|
+
### DeviceMockup
|
|
1125
|
+
|
|
1126
|
+
```tsx
|
|
1127
|
+
import { DeviceMockup } from "@synertech/ui";
|
|
1128
|
+
import type { DeviceType } from "@synertech/ui";
|
|
1129
|
+
|
|
1130
|
+
// Avec du contenu React
|
|
1131
|
+
<DeviceMockup
|
|
1132
|
+
device="phone" // "phone" | "android" | "tablet" | "laptop" | "desktop" | "watch" | "browser"
|
|
1133
|
+
orientation="portrait" // "portrait" | "landscape"
|
|
1134
|
+
theme="dark" // "dark" | "light"
|
|
1135
|
+
size="md" // "sm" | "md" | "lg"
|
|
1136
|
+
showStatusBar={true}
|
|
1137
|
+
statusBarTime="14:30"
|
|
1138
|
+
animated={false} // Auto-scroll du contenu
|
|
1139
|
+
scrollSpeed={30} // px/s pour l'animation
|
|
1140
|
+
frameColor="#1e40af" // Couleur du cadre (CSS)
|
|
1141
|
+
contentClassName="bg-white"
|
|
1142
|
+
>
|
|
1143
|
+
<div className="bg-gradient-to-b from-blue-500 to-blue-700 text-white h-full p-4">
|
|
1144
|
+
<h2 className="text-lg font-bold mt-6">Mon App</h2>
|
|
1145
|
+
<p className="text-sm opacity-80 mt-2">Bienvenue</p>
|
|
1146
|
+
</div>
|
|
1147
|
+
</DeviceMockup>
|
|
1148
|
+
|
|
1149
|
+
// Avec une image
|
|
1150
|
+
<DeviceMockup
|
|
1151
|
+
device="laptop"
|
|
1152
|
+
src="/screenshot.png"
|
|
1153
|
+
alt="Capture d'écran de l'application"
|
|
1154
|
+
theme="light"
|
|
1155
|
+
size="lg"
|
|
1156
|
+
/>
|
|
1157
|
+
|
|
1158
|
+
// Vitrine animée (défilement automatique)
|
|
1159
|
+
<DeviceMockup
|
|
1160
|
+
device="phone"
|
|
1161
|
+
animated
|
|
1162
|
+
scrollSpeed={20}
|
|
1163
|
+
showStatusBar
|
|
1164
|
+
>
|
|
1165
|
+
<div className="p-4 space-y-4">
|
|
1166
|
+
{/* Contenu plus grand que l'écran — défile automatiquement */}
|
|
1167
|
+
{items.map((item) => (
|
|
1168
|
+
<div key={item.id} className="bg-gray-100 rounded-lg p-3">{item.name}</div>
|
|
1169
|
+
))}
|
|
1170
|
+
</div>
|
|
1171
|
+
</DeviceMockup>
|
|
1172
|
+
|
|
1173
|
+
// Tous les appareils côte à côte
|
|
1174
|
+
<div className="flex flex-wrap gap-8 items-end">
|
|
1175
|
+
<DeviceMockup device="watch" size="sm" showStatusBar>...</DeviceMockup>
|
|
1176
|
+
<DeviceMockup device="phone" size="sm">...</DeviceMockup>
|
|
1177
|
+
<DeviceMockup device="tablet" size="sm">...</DeviceMockup>
|
|
1178
|
+
<DeviceMockup device="laptop" size="sm">...</DeviceMockup>
|
|
1179
|
+
<DeviceMockup device="desktop" size="sm">...</DeviceMockup>
|
|
1180
|
+
<DeviceMockup device="browser" size="sm">...</DeviceMockup>
|
|
1181
|
+
</div>
|
|
1182
|
+
```
|
|
1183
|
+
|
|
1184
|
+
**Détails par appareil :**
|
|
1185
|
+
- **phone** : notch/dynamic island (en haut portrait, à droite landscape), home indicator, bords arrondis
|
|
1186
|
+
- **android** : caméra dot, bords arrondis fins
|
|
1187
|
+
- **tablet** : caméra discrète, bezels fins
|
|
1188
|
+
- **laptop** : écran + barre charnière + base clavier
|
|
1189
|
+
- **desktop** : moniteur + pied + socle
|
|
1190
|
+
- **watch** : couronne latérale, bracelets haut/bas
|
|
1191
|
+
- **browser** : chrome avec feux tricolores et barre URL
|
|
1192
|
+
|
|
1193
|
+
**Notes :**
|
|
1194
|
+
- En mode `landscape`, le phone positionne le notch à droite (rotation réaliste)
|
|
1195
|
+
- `showStatusBar` fonctionne uniquement sur phone, android, tablet et watch
|
|
1196
|
+
- `animated` fait défiler le contenu automatiquement en boucle
|
|
1197
|
+
- `src` et `children` sont mutuellement exclusifs — `src` a priorité
|
|
1198
|
+
|
|
1199
|
+
### SchemaEditor
|
|
1200
|
+
|
|
1201
|
+
```tsx
|
|
1202
|
+
import { SchemaEditor } from "@synertech/ui";
|
|
1203
|
+
import type { SchemaValue, SchemaEditorRef } from "@synertech/ui";
|
|
1204
|
+
import { useRef } from "react";
|
|
1205
|
+
|
|
1206
|
+
const ref = useRef<SchemaEditorRef>(null);
|
|
1207
|
+
|
|
1208
|
+
<SchemaEditor
|
|
1209
|
+
ref={ref}
|
|
1210
|
+
defaultValue={{ tables: [], relations: [] }}
|
|
1211
|
+
onValueChange={(val) => console.log(val)}
|
|
1212
|
+
editable={true}
|
|
1213
|
+
draggableTables={true}
|
|
1214
|
+
panOnDrag={true}
|
|
1215
|
+
zoomOnWheel={true}
|
|
1216
|
+
showGrid={true}
|
|
1217
|
+
toolbar={true}
|
|
1218
|
+
legend={true}
|
|
1219
|
+
autoFit={false}
|
|
1220
|
+
/>
|
|
1221
|
+
|
|
1222
|
+
// API imperative
|
|
1223
|
+
ref.current?.fitView();
|
|
1224
|
+
ref.current?.exportSQL();
|
|
1225
|
+
ref.current?.autoLayout();
|
|
1226
|
+
ref.current?.addTable({ id: "users", name: "users", fields: [] });
|
|
1227
|
+
```
|
|
1228
|
+
|
|
1229
|
+
---
|
|
1230
|
+
|
|
1231
|
+
## Patterns essentiels
|
|
1232
|
+
|
|
1233
|
+
### 1. Personnalisation avec className
|
|
1234
|
+
|
|
1235
|
+
Tous les composants acceptent `className` pour surcharger les styles :
|
|
1236
|
+
|
|
1237
|
+
```tsx
|
|
1238
|
+
<Button className="bg-purple-600 hover:bg-purple-700 rounded-full">
|
|
1239
|
+
Custom
|
|
1240
|
+
</Button>
|
|
1241
|
+
|
|
1242
|
+
<Card className="border-2 border-indigo-500">
|
|
1243
|
+
<CardBody>Contenu</CardBody>
|
|
1244
|
+
</Card>
|
|
1245
|
+
```
|
|
1246
|
+
|
|
1247
|
+
### 2. Controlled vs Uncontrolled
|
|
1248
|
+
|
|
1249
|
+
Les composants de formulaire supportent les deux patterns :
|
|
1250
|
+
|
|
1251
|
+
```tsx
|
|
1252
|
+
// Controlled (vous gerez l'etat)
|
|
1253
|
+
const [value, setValue] = useState("");
|
|
1254
|
+
<Select value={value} onValueChange={setValue} options={opts} />
|
|
1255
|
+
|
|
1256
|
+
// Uncontrolled (etat interne)
|
|
1257
|
+
<Select defaultValue="fr" onValueChange={(v) => console.log(v)} options={opts} />
|
|
1258
|
+
```
|
|
1259
|
+
|
|
1260
|
+
### 3. Composants dans Server Components (Next.js)
|
|
1261
|
+
|
|
1262
|
+
Les composants **server-compatible** (sans `"use client"`) peuvent etre utilises directement dans les Server Components :
|
|
1263
|
+
|
|
1264
|
+
```tsx
|
|
1265
|
+
// app/page.tsx (Server Component)
|
|
1266
|
+
import { Card, CardBody, Avatar, Chip, Divider, Skeleton } from "@synertech/ui";
|
|
1267
|
+
|
|
1268
|
+
export default function Page() {
|
|
1269
|
+
return (
|
|
1270
|
+
<Card variant="outlined">
|
|
1271
|
+
<CardBody>
|
|
1272
|
+
<Avatar src="/photo.jpg" fallback="JD" />
|
|
1273
|
+
<Chip label="Active" color="success" />
|
|
1274
|
+
<Divider label="Details" />
|
|
1275
|
+
</CardBody>
|
|
1276
|
+
</Card>
|
|
1277
|
+
);
|
|
1278
|
+
}
|
|
1279
|
+
```
|
|
1280
|
+
|
|
1281
|
+
Les composants **non server-compatible** doivent etre utilises dans un Client Component :
|
|
1282
|
+
|
|
1283
|
+
```tsx
|
|
1284
|
+
// components/UserForm.tsx
|
|
1285
|
+
"use client";
|
|
1286
|
+
import { Select, Input, Checkbox } from "@synertech/ui";
|
|
1287
|
+
|
|
1288
|
+
export function UserForm() {
|
|
1289
|
+
// ... useState, handlers, etc.
|
|
1290
|
+
return (
|
|
1291
|
+
<form>
|
|
1292
|
+
<Input label="Nom" />
|
|
1293
|
+
<Select options={countries} />
|
|
1294
|
+
<Checkbox label="Accepter" />
|
|
1295
|
+
</form>
|
|
1296
|
+
);
|
|
1297
|
+
}
|
|
1298
|
+
```
|
|
1299
|
+
|
|
1300
|
+
### 4. Composition de formulaire idiomatique
|
|
1301
|
+
|
|
1302
|
+
```tsx
|
|
1303
|
+
"use client";
|
|
1304
|
+
import { Input, Select, Checkbox, RadioButton, Button, Label } from "@synertech/ui";
|
|
1305
|
+
import { useState } from "react";
|
|
1306
|
+
|
|
1307
|
+
export function ContactForm() {
|
|
1308
|
+
const [formData, setFormData] = useState({ name: "", email: "", subject: "", agree: false });
|
|
1309
|
+
|
|
1310
|
+
return (
|
|
1311
|
+
<form className="flex flex-col gap-4">
|
|
1312
|
+
<Input
|
|
1313
|
+
label="Nom complet"
|
|
1314
|
+
value={formData.name}
|
|
1315
|
+
onChange={(e) => setFormData(prev => ({ ...prev, name: e.target.value }))}
|
|
1316
|
+
required
|
|
1317
|
+
/>
|
|
1318
|
+
<Input
|
|
1319
|
+
label="Email"
|
|
1320
|
+
type="email"
|
|
1321
|
+
value={formData.email}
|
|
1322
|
+
onChange={(e) => setFormData(prev => ({ ...prev, email: e.target.value }))}
|
|
1323
|
+
error={!formData.email.includes("@")}
|
|
1324
|
+
helperText="Adresse email valide requise"
|
|
1325
|
+
/>
|
|
1326
|
+
<Select
|
|
1327
|
+
label="Sujet"
|
|
1328
|
+
options={[
|
|
1329
|
+
{ value: "support", label: "Support technique" },
|
|
1330
|
+
{ value: "sales", label: "Commercial" },
|
|
1331
|
+
{ value: "other", label: "Autre" },
|
|
1332
|
+
]}
|
|
1333
|
+
value={formData.subject}
|
|
1334
|
+
onValueChange={(v) => setFormData(prev => ({ ...prev, subject: v }))}
|
|
1335
|
+
placeholder="Choisir un sujet..."
|
|
1336
|
+
required
|
|
1337
|
+
/>
|
|
1338
|
+
<Checkbox
|
|
1339
|
+
label="J'accepte les conditions d'utilisation"
|
|
1340
|
+
checked={formData.agree}
|
|
1341
|
+
onChange={(checked) => setFormData(prev => ({ ...prev, agree: checked }))}
|
|
1342
|
+
/>
|
|
1343
|
+
<Button type="submit" fullWidth>Envoyer</Button>
|
|
1344
|
+
</form>
|
|
1345
|
+
);
|
|
1346
|
+
}
|
|
1347
|
+
```
|
|
1348
|
+
|
|
1349
|
+
### 5. Pattern de page complete
|
|
1350
|
+
|
|
1351
|
+
```tsx
|
|
1352
|
+
import { Header, Footer, Card, CardBody, Table, TableHeader, TableHeaderCell, TableBody, TableRow, TableCell, Chip, Pagination, Button, Breadcrumb } from "@synertech/ui";
|
|
1353
|
+
|
|
1354
|
+
// Server Component
|
|
1355
|
+
export default function DashboardPage() {
|
|
1356
|
+
return (
|
|
1357
|
+
<div className="min-h-screen flex flex-col">
|
|
1358
|
+
<Header
|
|
1359
|
+
logo={<Logo />}
|
|
1360
|
+
navItems={[{ label: "Dashboard", href: "/", active: true }]}
|
|
1361
|
+
sticky
|
|
1362
|
+
/>
|
|
1363
|
+
|
|
1364
|
+
<main className="flex-1 p-6">
|
|
1365
|
+
<Breadcrumb items={[{ label: "Accueil", href: "/" }, { label: "Utilisateurs" }]} />
|
|
1366
|
+
|
|
1367
|
+
<Card variant="outlined" className="mt-4">
|
|
1368
|
+
<CardBody>
|
|
1369
|
+
<Table>
|
|
1370
|
+
<TableHeader>
|
|
1371
|
+
<TableRow>
|
|
1372
|
+
<TableHeaderCell>Nom</TableHeaderCell>
|
|
1373
|
+
<TableHeaderCell>Statut</TableHeaderCell>
|
|
1374
|
+
</TableRow>
|
|
1375
|
+
</TableHeader>
|
|
1376
|
+
<TableBody>
|
|
1377
|
+
<TableRow>
|
|
1378
|
+
<TableCell>Jean Dupont</TableCell>
|
|
1379
|
+
<TableCell><Chip label="Actif" color="success" size="sm" /></TableCell>
|
|
1380
|
+
</TableRow>
|
|
1381
|
+
</TableBody>
|
|
1382
|
+
</Table>
|
|
1383
|
+
</CardBody>
|
|
1384
|
+
</Card>
|
|
1385
|
+
|
|
1386
|
+
{/* Pagination doit etre dans un Client Component */}
|
|
1387
|
+
</main>
|
|
1388
|
+
|
|
1389
|
+
<Footer variant="bordered">
|
|
1390
|
+
<p>2024 Synertech</p>
|
|
1391
|
+
</Footer>
|
|
1392
|
+
</div>
|
|
1393
|
+
);
|
|
1394
|
+
}
|
|
1395
|
+
```
|
|
1396
|
+
|
|
1397
|
+
---
|
|
1398
|
+
|
|
1399
|
+
### 6. Dialogues accessibles (Modal / Drawer / Sidebar mobile)
|
|
1400
|
+
|
|
1401
|
+
`Modal`, `Drawer` et la `Sidebar` en mode mobile implementent le contrat complet
|
|
1402
|
+
d'un dialogue modal. Il n'y a rien a rajouter cote application :
|
|
1403
|
+
|
|
1404
|
+
| Besoin | Fourni par la librairie |
|
|
1405
|
+
| --- | --- |
|
|
1406
|
+
| Role et modalite | `role="dialog"` + `aria-modal="true"` |
|
|
1407
|
+
| Nom accessible | `aria-labelledby` sur le `title`, sinon prop `aria-label` |
|
|
1408
|
+
| Description | `aria-describedby` sur la `description` (Modal) |
|
|
1409
|
+
| Focus initial | le dialogue lui-meme (`initialFocus="first"` ou une `ref` au besoin) |
|
|
1410
|
+
| Confinement | Tab / Maj+Tab bouclent dans le dialogue (`trapFocus`) |
|
|
1411
|
+
| Echap | ferme le dialogue du dessus (`closeOnEscape`) |
|
|
1412
|
+
| Retour du focus | le declencheur reprend le focus a la fermeture (`returnFocus`) |
|
|
1413
|
+
| Fond inerte | le reste de la page passe en `inert` + `aria-hidden`, scroll bloque |
|
|
1414
|
+
| Dialogues imbriques | pile interne : Echap et le focus ne concernent que le dialogue du dessus |
|
|
1415
|
+
|
|
1416
|
+
En dessous de `mobileBreakpoint`, la `Sidebar` bascule en tiroir modal et suit le
|
|
1417
|
+
meme contrat ; fermee, le panneau hors ecran passe en `inert` donc il sort de
|
|
1418
|
+
l'ordre de tabulation.
|
|
1419
|
+
|
|
1420
|
+
```tsx
|
|
1421
|
+
// Le declencheur reprend le focus automatiquement a la fermeture
|
|
1422
|
+
<button onClick={() => setOpen(true)}>Ouvrir la tache</button>
|
|
1423
|
+
|
|
1424
|
+
<Modal
|
|
1425
|
+
isOpen={open}
|
|
1426
|
+
onClose={() => setOpen(false)}
|
|
1427
|
+
title="Detail de la tache"
|
|
1428
|
+
description="Tous les champs sont obligatoires."
|
|
1429
|
+
>
|
|
1430
|
+
<TaskForm />
|
|
1431
|
+
</Modal>
|
|
1432
|
+
```
|
|
1433
|
+
|
|
1434
|
+
Cas particuliers :
|
|
1435
|
+
|
|
1436
|
+
- **Focus sur un champ precis a l'ouverture** : `initialFocus={inputRef}`.
|
|
1437
|
+
- **Focus rendu ailleurs qu'au declencheur** (element supprime par l'action) :
|
|
1438
|
+
`returnFocus={autreRef}`.
|
|
1439
|
+
- **Zone qui doit rester active derriere le dialogue** (conteneur de toasts,
|
|
1440
|
+
region `aria-live`) : ajouter l'attribut `data-synertech-inert-ignore` sur son
|
|
1441
|
+
element racine.
|
|
1442
|
+
- **Menu / popover maison rendu dans un portail** depuis l'interieur du
|
|
1443
|
+
dialogue : le declarer avec `registerFloatingLayer(element, () => trigger)`,
|
|
1444
|
+
sinon il sera rendu inerte avec le fond. Les composants de la librairie
|
|
1445
|
+
(`Dropdown`, `Select`) le font deja : Echap y ferme d'abord le menu, un second
|
|
1446
|
+
Echap ferme le dialogue.
|
|
1447
|
+
- **Dialogue maison** : le hook `useDialogA11y` est exporte et fournit le meme
|
|
1448
|
+
comportement (`const { dialogRef, dialogProps } = useDialogA11y({ open, onClose })`).
|
|
1449
|
+
|
|
1450
|
+
### 7. Traduction des libelles integres (FR / EN)
|
|
1451
|
+
|
|
1452
|
+
Les libelles rendus par la librairie (« Close modal », « Close drawer »,
|
|
1453
|
+
« Confirm », « Cancel », plein ecran) sont traduisibles globalement via
|
|
1454
|
+
`UILabelsProvider`, ou au cas par cas via les props du composant (la prop gagne
|
|
1455
|
+
toujours).
|
|
1456
|
+
|
|
1457
|
+
```tsx
|
|
1458
|
+
// app/layout.tsx (ou n'importe quel niveau de l'arbre)
|
|
1459
|
+
import { UILabelsProvider, frenchUILabels, defaultUILabels } from "@synertech/ui";
|
|
1460
|
+
|
|
1461
|
+
<UILabelsProvider labels={locale === "fr" ? frenchUILabels : defaultUILabels}>
|
|
1462
|
+
{children}
|
|
1463
|
+
</UILabelsProvider>;
|
|
1464
|
+
|
|
1465
|
+
// Override partiel (les autres libelles restent ceux du provider parent)
|
|
1466
|
+
<UILabelsProvider labels={{ closeModal: "Fermer le detail" }}>
|
|
1467
|
+
<TaskModal />
|
|
1468
|
+
</UILabelsProvider>;
|
|
1469
|
+
|
|
1470
|
+
// Override ponctuel
|
|
1471
|
+
<Modal closeLabel="Fermer le detail" ... />
|
|
1472
|
+
<Drawer closeLabel="Fermer le menu" ... />
|
|
1473
|
+
<Sidebar closeLabel="Fermer la navigation" aria-label="Navigation principale" ... />
|
|
1474
|
+
```
|
|
1475
|
+
|
|
1476
|
+
Cles disponibles : `closeModal`, `closeDrawer`, `closeSidebar`, `openSidebar`,
|
|
1477
|
+
`expandSidebar`, `collapseSidebar`, `sidebar`, `enterFullScreen`,
|
|
1478
|
+
`exitFullScreen`, `confirm`, `cancel`.
|
|
1479
|
+
|
|
1480
|
+
## Anti-patterns a eviter
|
|
1481
|
+
|
|
1482
|
+
### 1. Ne pas importer les styles
|
|
1483
|
+
|
|
1484
|
+
```tsx
|
|
1485
|
+
// MAUVAIS - les composants n'auront pas de styles
|
|
1486
|
+
import { Button } from "@synertech/ui";
|
|
1487
|
+
|
|
1488
|
+
// BON - importer les styles dans le layout racine
|
|
1489
|
+
import "@synertech/ui/styles.css";
|
|
1490
|
+
```
|
|
1491
|
+
|
|
1492
|
+
### 2. Ne pas utiliser className directement sur les sous-composants composables
|
|
1493
|
+
|
|
1494
|
+
```tsx
|
|
1495
|
+
// MAUVAIS - risque de casser le layout interne
|
|
1496
|
+
<DropdownContent style={{ position: "absolute", top: 100 }}>
|
|
1497
|
+
|
|
1498
|
+
// BON - utiliser les props dediees
|
|
1499
|
+
<DropdownContent side="bottom" sideOffset={4} align="start">
|
|
1500
|
+
```
|
|
1501
|
+
|
|
1502
|
+
### 3. Ne pas creer de wrappers inutiles pour les composants server-compatible
|
|
1503
|
+
|
|
1504
|
+
```tsx
|
|
1505
|
+
// MAUVAIS - ajoute "use client" sans raison
|
|
1506
|
+
"use client";
|
|
1507
|
+
import { Card, CardBody } from "@synertech/ui";
|
|
1508
|
+
export function MyCard({ children }) {
|
|
1509
|
+
return <Card><CardBody>{children}</CardBody></Card>;
|
|
1510
|
+
}
|
|
1511
|
+
|
|
1512
|
+
// BON - utiliser directement dans un Server Component
|
|
1513
|
+
import { Card, CardBody } from "@synertech/ui";
|
|
1514
|
+
// Pas besoin de "use client" car Card est server-compatible
|
|
1515
|
+
```
|
|
1516
|
+
|
|
1517
|
+
### 4. Ne pas re-implementer ce qui existe deja
|
|
1518
|
+
|
|
1519
|
+
```tsx
|
|
1520
|
+
// MAUVAIS
|
|
1521
|
+
<div className="flex items-center gap-2">
|
|
1522
|
+
<span className="w-2 h-2 rounded-full bg-green-500" />
|
|
1523
|
+
<span>En ligne</span>
|
|
1524
|
+
</div>
|
|
1525
|
+
|
|
1526
|
+
// BON
|
|
1527
|
+
<Indicator variant="status" color="success" label="En ligne" />
|
|
1528
|
+
```
|
|
1529
|
+
|
|
1530
|
+
### 5. Ne pas oublier le pattern controlled pour les formulaires
|
|
1531
|
+
|
|
1532
|
+
```tsx
|
|
1533
|
+
// MAUVAIS - pas de gestion d'etat
|
|
1534
|
+
<Select options={opts} />
|
|
1535
|
+
// L'utilisateur ne peut pas recuperer la valeur selectionnee
|
|
1536
|
+
|
|
1537
|
+
// BON
|
|
1538
|
+
const [val, setVal] = useState("");
|
|
1539
|
+
<Select options={opts} value={val} onValueChange={setVal} />
|
|
1540
|
+
```
|
|
1541
|
+
|
|
1542
|
+
### 6. Utiliser les props de couleur au lieu de className quand disponible
|
|
1543
|
+
|
|
1544
|
+
```tsx
|
|
1545
|
+
// ACCEPTABLE mais moins idiomatique
|
|
1546
|
+
<Button className="bg-red-600 hover:bg-red-700 text-white">Supprimer</Button>
|
|
1547
|
+
|
|
1548
|
+
// MIEUX - utilise les props dediees
|
|
1549
|
+
<Button bgColor="bg-red-600 hover:bg-red-700" textColor="text-white">Supprimer</Button>
|
|
1550
|
+
```
|
|
1551
|
+
|
|
1552
|
+
### 7. Reimplementer l'accessibilite des dialogues cote application
|
|
1553
|
+
|
|
1554
|
+
```tsx
|
|
1555
|
+
// MAUVAIS - focus trap maison, Echap maison, wrapper role="dialog"
|
|
1556
|
+
<div role="dialog" aria-modal="true" ref={monFocusTrap}>
|
|
1557
|
+
<Modal isOpen={open} onClose={close}>...</Modal>
|
|
1558
|
+
</div>
|
|
1559
|
+
|
|
1560
|
+
// BON - le composant fournit deja role/aria-modal, focus, Tab, Echap,
|
|
1561
|
+
// retour du focus et fond inerte
|
|
1562
|
+
<Modal isOpen={open} onClose={close} title="Detail de la tache">...</Modal>
|
|
1563
|
+
```
|
|
1564
|
+
|
|
1565
|
+
Un dialogue vraiment specifique se construit avec le hook exporte
|
|
1566
|
+
`useDialogA11y`, pas avec un composant local qui duplique le comportement.
|
|
1567
|
+
|
|
1568
|
+
---
|
|
1569
|
+
|
|
1570
|
+
## Documentation interactive
|
|
1571
|
+
|
|
1572
|
+
La documentation Storybook avec tous les composants et leurs variantes est disponible a :
|
|
1573
|
+
**https://ui.synertechweb.com/**
|
|
1574
|
+
|
|
1575
|
+
Chaque composant a une page interactive avec :
|
|
1576
|
+
- Props table auto-generee
|
|
1577
|
+
- Exemples visuels de toutes les variantes
|
|
1578
|
+
- Playground interactif (Controls)
|