@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.
@@ -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)