@dsfrkit/cli 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +90 -0
  2. package/dist/index.js +1132 -0
  3. package/package.json +46 -0
package/README.md ADDED
@@ -0,0 +1,90 @@
1
+ # @dsfrkit/cli
2
+
3
+ CLI pour installer et copier les composants DSFR dans votre projet.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ # Via pnpm (recommandé)
9
+ pnpm dlx @dsfrkit/cli init
10
+
11
+ # Via npx
12
+ npx @dsfrkit/cli init
13
+
14
+ # Installation globale
15
+ pnpm add -g @dsfrkit/cli
16
+ ```
17
+
18
+ ## Commandes
19
+
20
+ ### `init`
21
+
22
+ Initialise le projet avec la configuration DSFR.
23
+
24
+ ```bash
25
+ dsfrkit init
26
+ ```
27
+
28
+ Cette commande :
29
+ - ✅ Crée le dossier `src/components/ui`
30
+ - ✅ Crée le fichier `src/lib/utils.ts` avec la fonction `cn()`
31
+ - ✅ Configure `tailwind.config.js` avec le preset DSFR
32
+ - ✅ Installe les dépendances nécessaires
33
+
34
+ ### `add`
35
+
36
+ Ajoute des composants au projet.
37
+
38
+ ```bash
39
+ # Ajouter des composants spécifiques
40
+ dsfrkit add button alert
41
+
42
+ # Mode interactif
43
+ dsfrkit add
44
+ ```
45
+
46
+ Cette commande copie les composants dans `src/components/ui/` avec toutes leurs dépendances.
47
+
48
+ ## Workflow recommandé
49
+
50
+ 1. **Initialiser le projet**
51
+ ```bash
52
+ pnpm dlx @dsfrkit/cli init
53
+ ```
54
+
55
+ 2. **Ajouter des composants**
56
+ ```bash
57
+ pnpm dlx @dsfrkit/cli add button alert card
58
+ ```
59
+
60
+ 3. **Utiliser les composants**
61
+ ```tsx
62
+ import { Button } from '@/components/ui/button'
63
+ import { Alert } from '@/components/ui/alert'
64
+
65
+ function App() {
66
+ return (
67
+ <div>
68
+ <Button variant="primary">Valider</Button>
69
+ <Alert variant="success">Succès !</Alert>
70
+ </div>
71
+ )
72
+ }
73
+ ```
74
+
75
+ 4. **Personnaliser selon vos besoins**
76
+
77
+ Les composants sont copiés dans votre projet, vous pouvez les modifier librement !
78
+
79
+ ## Composants disponibles
80
+
81
+ - ✅ `button` - Bouton DSFR avec variants
82
+ - ✅ `alert` - Alerte DSFR
83
+ - 🚧 `card` - Carte (à venir)
84
+ - 🚧 `input` - Champ de formulaire (à venir)
85
+ - 🚧 `modal` - Modale (à venir)
86
+ - 🚧 Plus de composants bientôt...
87
+
88
+ ## License
89
+
90
+ ETALAB-2.0
package/dist/index.js ADDED
@@ -0,0 +1,1132 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { Command } from "commander";
5
+
6
+ // src/commands/add.ts
7
+ import path2 from "path";
8
+ import { fileURLToPath } from "url";
9
+ import chalk2 from "chalk";
10
+ import fs2 from "fs-extra";
11
+ import ora2 from "ora";
12
+ import prompts from "prompts";
13
+
14
+ // src/templates.ts
15
+ var componentTemplates = {
16
+ button: `import * as React from 'react'
17
+ import { cva, type VariantProps } from 'class-variance-authority'
18
+ import { cn } from '@/lib/utils'
19
+
20
+ const buttonVariants = cva(
21
+ 'inline-flex items-center justify-center gap-2 rounded-md font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50',
22
+ {
23
+ variants: {
24
+ variant: {
25
+ primary: 'bg-blue-france-main text-white hover:bg-blue-france-625 focus-visible:ring-blue-france-main',
26
+ secondary: 'bg-red-marianne-main text-white hover:bg-red-marianne-625 focus-visible:ring-red-marianne-main',
27
+ tertiary: 'border-2 border-blue-france-main text-blue-france-main hover:bg-blue-france-50 focus-visible:ring-blue-france-main',
28
+ ghost: 'text-blue-france-main hover:bg-blue-france-50 focus-visible:ring-blue-france-main',
29
+ error: 'bg-error-main text-white hover:bg-error-625 focus-visible:ring-error-main',
30
+ success: 'bg-success-main text-white hover:bg-success-625 focus-visible:ring-success-main',
31
+ warning: 'bg-warning-main text-white hover:bg-warning-625 focus-visible:ring-warning-main',
32
+ },
33
+ size: {
34
+ sm: 'h-9 px-3 text-sm',
35
+ md: 'h-11 px-5 text-base',
36
+ lg: 'h-14 px-8 text-lg',
37
+ },
38
+ fullWidth: {
39
+ true: 'w-full',
40
+ false: '',
41
+ },
42
+ },
43
+ defaultVariants: {
44
+ variant: 'primary',
45
+ size: 'md',
46
+ fullWidth: false,
47
+ },
48
+ }
49
+ )
50
+
51
+ export interface ButtonProps
52
+ extends React.ButtonHTMLAttributes<HTMLButtonElement>,
53
+ VariantProps<typeof buttonVariants> {}
54
+
55
+ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
56
+ ({ className, variant, size, fullWidth, ...props }, ref) => {
57
+ return (
58
+ <button
59
+ className={cn(buttonVariants({ variant, size, fullWidth, className }))}
60
+ ref={ref}
61
+ {...props}
62
+ />
63
+ )
64
+ }
65
+ )
66
+
67
+ Button.displayName = 'Button'
68
+
69
+ export { Button, buttonVariants }
70
+ `,
71
+ alert: `import * as React from 'react'
72
+ import { cva, type VariantProps } from 'class-variance-authority'
73
+ import { cn } from '@/lib/utils'
74
+
75
+ const alertVariants = cva(
76
+ 'relative w-full rounded-lg border p-4',
77
+ {
78
+ variants: {
79
+ variant: {
80
+ info: 'border-info-main bg-info-50 text-info-main',
81
+ success: 'border-success-main bg-success-50 text-success-main',
82
+ warning: 'border-warning-main bg-warning-50 text-warning-main',
83
+ error: 'border-error-main bg-error-50 text-error-main',
84
+ },
85
+ },
86
+ defaultVariants: {
87
+ variant: 'info',
88
+ },
89
+ }
90
+ )
91
+
92
+ export interface AlertProps
93
+ extends React.HTMLAttributes<HTMLDivElement>,
94
+ VariantProps<typeof alertVariants> {
95
+ title?: string
96
+ }
97
+
98
+ const Alert = React.forwardRef<HTMLDivElement, AlertProps>(
99
+ ({ className, variant, title, children, ...props }, ref) => {
100
+ return (
101
+ <div
102
+ ref={ref}
103
+ role="alert"
104
+ className={cn(alertVariants({ variant }), className)}
105
+ {...props}
106
+ >
107
+ {title && (
108
+ <h5 className="mb-1 font-bold leading-none tracking-tight">
109
+ {title}
110
+ </h5>
111
+ )}
112
+ {children && <div className="text-sm">{children}</div>}
113
+ </div>
114
+ )
115
+ }
116
+ )
117
+
118
+ Alert.displayName = 'Alert'
119
+
120
+ export { Alert, alertVariants }
121
+ `,
122
+ card: `import * as React from 'react'
123
+ import { cva, type VariantProps } from 'class-variance-authority'
124
+ import { cn } from '@/lib/utils'
125
+
126
+ const cardVariants = cva(
127
+ 'rounded-lg border bg-white transition-shadow',
128
+ {
129
+ variants: {
130
+ variant: {
131
+ default: 'border-grey-200 shadow-sm hover:shadow-md',
132
+ bordered: 'border-2 border-blue-france-main',
133
+ elevated: 'border-grey-200 shadow-lg',
134
+ ghost: 'border-transparent',
135
+ },
136
+ padding: {
137
+ none: 'p-0',
138
+ sm: 'p-4',
139
+ md: 'p-6',
140
+ lg: 'p-8',
141
+ },
142
+ },
143
+ defaultVariants: {
144
+ variant: 'default',
145
+ padding: 'md',
146
+ },
147
+ }
148
+ )
149
+
150
+ export interface CardProps
151
+ extends React.HTMLAttributes<HTMLDivElement>,
152
+ VariantProps<typeof cardVariants> {}
153
+
154
+ const Card = React.forwardRef<HTMLDivElement, CardProps>(
155
+ ({ className, variant, padding, ...props }, ref) => {
156
+ return (
157
+ <div
158
+ ref={ref}
159
+ className={cn(cardVariants({ variant, padding, className }))}
160
+ {...props}
161
+ />
162
+ )
163
+ }
164
+ )
165
+
166
+ Card.displayName = 'Card'
167
+
168
+ const CardHeader = React.forwardRef<
169
+ HTMLDivElement,
170
+ React.HTMLAttributes<HTMLDivElement>
171
+ >(({ className, ...props }, ref) => (
172
+ <div
173
+ ref={ref}
174
+ className={cn('flex flex-col space-y-1.5', className)}
175
+ {...props}
176
+ />
177
+ ))
178
+ CardHeader.displayName = 'CardHeader'
179
+
180
+ const CardTitle = React.forwardRef<
181
+ HTMLHeadingElement,
182
+ React.HTMLAttributes<HTMLHeadingElement>
183
+ >(({ className, ...props }, ref) => (
184
+ <h3
185
+ ref={ref}
186
+ className={cn(
187
+ 'text-2xl font-bold leading-none tracking-tight text-blue-france-main',
188
+ className
189
+ )}
190
+ {...props}
191
+ />
192
+ ))
193
+ CardTitle.displayName = 'CardTitle'
194
+
195
+ const CardDescription = React.forwardRef<
196
+ HTMLParagraphElement,
197
+ React.HTMLAttributes<HTMLParagraphElement>
198
+ >(({ className, ...props }, ref) => (
199
+ <p
200
+ ref={ref}
201
+ className={cn('text-sm text-grey-500', className)}
202
+ {...props}
203
+ />
204
+ ))
205
+ CardDescription.displayName = 'CardDescription'
206
+
207
+ const CardContent = React.forwardRef<
208
+ HTMLDivElement,
209
+ React.HTMLAttributes<HTMLDivElement>
210
+ >(({ className, ...props }, ref) => (
211
+ <div ref={ref} className={cn('pt-0', className)} {...props} />
212
+ ))
213
+ CardContent.displayName = 'CardContent'
214
+
215
+ const CardFooter = React.forwardRef<
216
+ HTMLDivElement,
217
+ React.HTMLAttributes<HTMLDivElement>
218
+ >(({ className, ...props }, ref) => (
219
+ <div
220
+ ref={ref}
221
+ className={cn('flex items-center pt-4', className)}
222
+ {...props}
223
+ />
224
+ ))
225
+ CardFooter.displayName = 'CardFooter'
226
+
227
+ export {
228
+ Card,
229
+ CardHeader,
230
+ CardTitle,
231
+ CardDescription,
232
+ CardContent,
233
+ CardFooter,
234
+ cardVariants,
235
+ }
236
+ `,
237
+ input: `import * as React from 'react'
238
+ import { cva, type VariantProps } from 'class-variance-authority'
239
+ import { cn } from '@/lib/utils'
240
+
241
+ const inputVariants = cva(
242
+ 'flex w-full rounded-md border font-marianne text-base transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-grey-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
243
+ {
244
+ variants: {
245
+ variant: {
246
+ default: 'border-grey-300 bg-white focus-visible:ring-blue-france-main',
247
+ error: 'border-error-main bg-error-50 focus-visible:ring-error-main',
248
+ success: 'border-success-main bg-success-50 focus-visible:ring-success-main',
249
+ },
250
+ inputSize: {
251
+ sm: 'h-9 px-3 py-1 text-sm',
252
+ md: 'h-11 px-4 py-2',
253
+ lg: 'h-14 px-5 py-3 text-lg',
254
+ },
255
+ },
256
+ defaultVariants: {
257
+ variant: 'default',
258
+ inputSize: 'md',
259
+ },
260
+ }
261
+ )
262
+
263
+ export interface InputProps
264
+ extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'size'>,
265
+ VariantProps<typeof inputVariants> {
266
+ label?: string
267
+ error?: string
268
+ hint?: string
269
+ }
270
+
271
+ const Input = React.forwardRef<HTMLInputElement, InputProps>(
272
+ ({ className, variant, inputSize, label, error, hint, id, ...props }, ref) => {
273
+ const inputId = id || React.useId()
274
+ const errorId = \`\${inputId}-error\`
275
+ const hintId = \`\${inputId}-hint\`
276
+
277
+ return (
278
+ <div className="w-full space-y-2">
279
+ {label && (
280
+ <label
281
+ htmlFor={inputId}
282
+ className="block text-sm font-medium text-grey-850"
283
+ >
284
+ {label}
285
+ {props.required && <span className="text-error-main ml-1">*</span>}
286
+ </label>
287
+ )}
288
+
289
+ {hint && !error && (
290
+ <p id={hintId} className="text-sm text-grey-500">
291
+ {hint}
292
+ </p>
293
+ )}
294
+
295
+ <input
296
+ id={inputId}
297
+ className={cn(
298
+ inputVariants({ variant: error ? 'error' : variant, inputSize, className })
299
+ )}
300
+ ref={ref}
301
+ aria-invalid={error ? 'true' : 'false'}
302
+ aria-describedby={
303
+ error ? errorId : hint ? hintId : undefined
304
+ }
305
+ {...props}
306
+ />
307
+
308
+ {error && (
309
+ <p id={errorId} className="text-sm text-error-main font-medium">
310
+ {error}
311
+ </p>
312
+ )}
313
+ </div>
314
+ )
315
+ }
316
+ )
317
+
318
+ Input.displayName = 'Input'
319
+
320
+ const textareaVariants = cva(
321
+ 'flex min-h-[80px] w-full rounded-md border font-marianne text-base transition-colors placeholder:text-grey-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 resize-y',
322
+ {
323
+ variants: {
324
+ variant: {
325
+ default: 'border-grey-300 bg-white focus-visible:ring-blue-france-main',
326
+ error: 'border-error-main bg-error-50 focus-visible:ring-error-main',
327
+ success: 'border-success-main bg-success-50 focus-visible:ring-success-main',
328
+ },
329
+ },
330
+ defaultVariants: {
331
+ variant: 'default',
332
+ },
333
+ }
334
+ )
335
+
336
+ export interface TextareaProps
337
+ extends React.TextareaHTMLAttributes<HTMLTextAreaElement>,
338
+ VariantProps<typeof textareaVariants> {
339
+ label?: string
340
+ error?: string
341
+ hint?: string
342
+ }
343
+
344
+ const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
345
+ ({ className, variant, label, error, hint, id, ...props }, ref) => {
346
+ const textareaId = id || React.useId()
347
+ const errorId = \`\${textareaId}-error\`
348
+ const hintId = \`\${textareaId}-hint\`
349
+
350
+ return (
351
+ <div className="w-full space-y-2">
352
+ {label && (
353
+ <label
354
+ htmlFor={textareaId}
355
+ className="block text-sm font-medium text-grey-850"
356
+ >
357
+ {label}
358
+ {props.required && <span className="text-error-main ml-1">*</span>}
359
+ </label>
360
+ )}
361
+
362
+ {hint && !error && (
363
+ <p id={hintId} className="text-sm text-grey-500">
364
+ {hint}
365
+ </p>
366
+ )}
367
+
368
+ <textarea
369
+ id={textareaId}
370
+ className={cn(
371
+ textareaVariants({ variant: error ? 'error' : variant, className }),
372
+ 'px-4 py-2'
373
+ )}
374
+ ref={ref}
375
+ aria-invalid={error ? 'true' : 'false'}
376
+ aria-describedby={
377
+ error ? errorId : hint ? hintId : undefined
378
+ }
379
+ {...props}
380
+ />
381
+
382
+ {error && (
383
+ <p id={errorId} className="text-sm text-error-main font-medium">
384
+ {error}
385
+ </p>
386
+ )}
387
+ </div>
388
+ )
389
+ }
390
+ )
391
+
392
+ Textarea.displayName = 'Textarea'
393
+
394
+ export { Input, inputVariants, Textarea, textareaVariants }
395
+ `,
396
+ modal: `import * as React from 'react'
397
+ import * as DialogPrimitive from '@radix-ui/react-dialog'
398
+ import { cva, type VariantProps } from 'class-variance-authority'
399
+ import { cn } from '@/lib/utils'
400
+
401
+ const Modal = DialogPrimitive.Root
402
+ const ModalTrigger = DialogPrimitive.Trigger
403
+ const ModalPortal = DialogPrimitive.Portal
404
+ const ModalClose = DialogPrimitive.Close
405
+
406
+ const ModalOverlay = React.forwardRef<
407
+ React.ElementRef<typeof DialogPrimitive.Overlay>,
408
+ React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
409
+ >(({ className, ...props }, ref) => (
410
+ <DialogPrimitive.Overlay
411
+ ref={ref}
412
+ className={cn(
413
+ 'fixed inset-0 z-50 bg-grey-1000/50 backdrop-blur-sm',
414
+ className
415
+ )}
416
+ {...props}
417
+ />
418
+ ))
419
+ ModalOverlay.displayName = DialogPrimitive.Overlay.displayName
420
+
421
+ const modalContentVariants = cva(
422
+ 'fixed left-[50%] top-[50%] z-50 grid w-full translate-x-[-50%] translate-y-[-50%] gap-4 border bg-white p-6 shadow-2xl rounded-lg border-grey-200',
423
+ {
424
+ variants: {
425
+ size: {
426
+ sm: 'max-w-sm',
427
+ md: 'max-w-lg',
428
+ lg: 'max-w-2xl',
429
+ xl: 'max-w-4xl',
430
+ full: 'max-w-[95vw] max-h-[95vh]',
431
+ },
432
+ },
433
+ defaultVariants: {
434
+ size: 'md',
435
+ },
436
+ }
437
+ )
438
+
439
+ export interface ModalContentProps
440
+ extends React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>,
441
+ VariantProps<typeof modalContentVariants> {}
442
+
443
+ const ModalContent = React.forwardRef<
444
+ React.ElementRef<typeof DialogPrimitive.Content>,
445
+ ModalContentProps
446
+ >(({ className, children, size, ...props }, ref) => (
447
+ <ModalPortal>
448
+ <ModalOverlay />
449
+ <DialogPrimitive.Content
450
+ ref={ref}
451
+ className={cn(modalContentVariants({ size, className }))}
452
+ {...props}
453
+ >
454
+ {children}
455
+ <DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 hover:opacity-100">
456
+ <svg className="h-4 w-4" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
457
+ <path d="M18 6 6 18M6 6l12 12" />
458
+ </svg>
459
+ <span className="sr-only">Fermer</span>
460
+ </DialogPrimitive.Close>
461
+ </DialogPrimitive.Content>
462
+ </ModalPortal>
463
+ ))
464
+ ModalContent.displayName = DialogPrimitive.Content.displayName
465
+
466
+ const ModalHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
467
+ <div className={cn('flex flex-col space-y-1.5', className)} {...props} />
468
+ )
469
+ ModalHeader.displayName = 'ModalHeader'
470
+
471
+ const ModalFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
472
+ <div className={cn('flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2', className)} {...props} />
473
+ )
474
+ ModalFooter.displayName = 'ModalFooter'
475
+
476
+ const ModalTitle = React.forwardRef<
477
+ React.ElementRef<typeof DialogPrimitive.Title>,
478
+ React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
479
+ >(({ className, ...props }, ref) => (
480
+ <DialogPrimitive.Title
481
+ ref={ref}
482
+ className={cn('text-2xl font-bold text-blue-france-main', className)}
483
+ {...props}
484
+ />
485
+ ))
486
+ ModalTitle.displayName = DialogPrimitive.Title.displayName
487
+
488
+ const ModalDescription = React.forwardRef<
489
+ React.ElementRef<typeof DialogPrimitive.Description>,
490
+ React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
491
+ >(({ className, ...props }, ref) => (
492
+ <DialogPrimitive.Description
493
+ ref={ref}
494
+ className={cn('text-sm text-grey-500', className)}
495
+ {...props}
496
+ />
497
+ ))
498
+ ModalDescription.displayName = DialogPrimitive.Description.displayName
499
+
500
+ export {
501
+ Modal,
502
+ ModalPortal,
503
+ ModalOverlay,
504
+ ModalClose,
505
+ ModalTrigger,
506
+ ModalContent,
507
+ ModalHeader,
508
+ ModalFooter,
509
+ ModalTitle,
510
+ ModalDescription,
511
+ }
512
+ `,
513
+ select: `import * as React from 'react'
514
+ import * as SelectPrimitive from '@radix-ui/react-select'
515
+ import { cn } from '@/lib/utils'
516
+
517
+ const Select = SelectPrimitive.Root
518
+ const SelectGroup = SelectPrimitive.Group
519
+ const SelectValue = SelectPrimitive.Value
520
+
521
+ const SelectTrigger = React.forwardRef<
522
+ React.ElementRef<typeof SelectPrimitive.Trigger>,
523
+ React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
524
+ >(({ className, children, ...props }, ref) => (
525
+ <SelectPrimitive.Trigger
526
+ ref={ref}
527
+ className={cn(
528
+ 'flex h-11 w-full items-center justify-between rounded-md border border-grey-300 bg-white px-4 py-2 text-base',
529
+ className
530
+ )}
531
+ {...props}
532
+ >
533
+ {children}
534
+ <SelectPrimitive.Icon asChild>
535
+ <svg className="h-4 w-4 opacity-50" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
536
+ <path d="m6 9 6 6 6-6" />
537
+ </svg>
538
+ </SelectPrimitive.Icon>
539
+ </SelectPrimitive.Trigger>
540
+ ))
541
+ SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
542
+
543
+ const SelectContent = React.forwardRef<
544
+ React.ElementRef<typeof SelectPrimitive.Content>,
545
+ React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
546
+ >(({ className, children, position = 'popper', ...props }, ref) => (
547
+ <SelectPrimitive.Portal>
548
+ <SelectPrimitive.Content
549
+ ref={ref}
550
+ className={cn(
551
+ 'relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border border-grey-200 bg-white shadow-md',
552
+ className
553
+ )}
554
+ position={position}
555
+ {...props}
556
+ >
557
+ <SelectPrimitive.Viewport className="p-1">
558
+ {children}
559
+ </SelectPrimitive.Viewport>
560
+ </SelectPrimitive.Content>
561
+ </SelectPrimitive.Portal>
562
+ ))
563
+ SelectContent.displayName = SelectPrimitive.Content.displayName
564
+
565
+ const SelectLabel = React.forwardRef<
566
+ React.ElementRef<typeof SelectPrimitive.Label>,
567
+ React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
568
+ >(({ className, ...props }, ref) => (
569
+ <SelectPrimitive.Label
570
+ ref={ref}
571
+ className={cn('px-2 py-1.5 text-sm font-semibold', className)}
572
+ {...props}
573
+ />
574
+ ))
575
+ SelectLabel.displayName = SelectPrimitive.Label.displayName
576
+
577
+ const SelectItem = React.forwardRef<
578
+ React.ElementRef<typeof SelectPrimitive.Item>,
579
+ React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
580
+ >(({ className, children, ...props }, ref) => (
581
+ <SelectPrimitive.Item
582
+ ref={ref}
583
+ className={cn(
584
+ 'relative flex cursor-pointer select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-blue-france-50',
585
+ className
586
+ )}
587
+ {...props}
588
+ >
589
+ <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
590
+ <SelectPrimitive.ItemIndicator>
591
+ <svg className="h-4 w-4" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
592
+ <path d="M20 6 9 17l-5-5" />
593
+ </svg>
594
+ </SelectPrimitive.ItemIndicator>
595
+ </span>
596
+ <SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
597
+ </SelectPrimitive.Item>
598
+ ))
599
+ SelectItem.displayName = SelectPrimitive.Item.displayName
600
+
601
+ export {
602
+ Select,
603
+ SelectGroup,
604
+ SelectValue,
605
+ SelectTrigger,
606
+ SelectContent,
607
+ SelectLabel,
608
+ SelectItem,
609
+ }
610
+ `,
611
+ "theme-artwork": `import type * as React from 'react'
612
+
613
+ export const ThemeArtworkLight = (props: React.SVGProps<SVGSVGElement>) => (
614
+ <svg aria-hidden="true" className="fr-artwork" viewBox="0 0 80 80" width="80px" height="80px" {...props}>
615
+ <use className="fr-artwork-decorative" href="/dist/artwork/pictograms/environment/sun.svg#artwork-decorative" />
616
+ <use className="fr-artwork-minor" href="/dist/artwork/pictograms/environment/sun.svg#artwork-minor" />
617
+ <use className="fr-artwork-major" href="/dist/artwork/pictograms/environment/sun.svg#artwork-major" />
618
+ </svg>
619
+ )
620
+
621
+ export const ThemeArtworkDark = (props: React.SVGProps<SVGSVGElement>) => (
622
+ <svg aria-hidden="true" className="fr-artwork" viewBox="0 0 80 80" width="80px" height="80px" {...props}>
623
+ <use className="fr-artwork-decorative" href="/dist/artwork/pictograms/environment/moon.svg#artwork-decorative" />
624
+ <use className="fr-artwork-minor" href="/dist/artwork/pictograms/environment/moon.svg#artwork-minor" />
625
+ <use className="fr-artwork-major" href="/dist/artwork/pictograms/environment/moon.svg#artwork-major" />
626
+ </svg>
627
+ )
628
+
629
+ export const ThemeArtworkSystem = (props: React.SVGProps<SVGSVGElement>) => (
630
+ <svg aria-hidden="true" className="fr-artwork" viewBox="0 0 80 80" width="80px" height="80px" {...props}>
631
+ <use className="fr-artwork-decorative" href="/dist/artwork/pictograms/system/system.svg#artwork-decorative" />
632
+ <use className="fr-artwork-minor" href="/dist/artwork/pictograms/system/system.svg#artwork-minor" />
633
+ <use className="fr-artwork-major" href="/dist/artwork/pictograms/system/system.svg#artwork-major" />
634
+ </svg>
635
+ )
636
+ `,
637
+ "theme-toggle": `import * as React from 'react'
638
+ import { useTheme } from 'next-themes'
639
+ import { Button, type ButtonProps } from './button'
640
+ import { Modal, ModalContent, ModalDescription, ModalHeader, ModalTitle, ModalTrigger } from './modal'
641
+ import { RadioGroup, RadioGroupItem } from './radio'
642
+ import { ThemeArtworkDark, ThemeArtworkLight, ThemeArtworkSystem } from './theme-artwork'
643
+
644
+ export interface ThemeToggleProps extends Omit<ButtonProps, 'icon' | 'iconPosition'> {
645
+ /**
646
+ * Si true, le bouton n'affiche que l'ic\xF4ne, sans le texte.
647
+ */
648
+ iconOnly?: boolean
649
+ }
650
+
651
+ /**
652
+ * Composant de s\xE9lection du th\xE8me (Param\xE8tres d'affichage)
653
+ */
654
+ export function ThemeToggle({
655
+ size = 'md',
656
+ iconOnly = false,
657
+ className,
658
+ variant = 'ghost',
659
+ ...props
660
+ }: ThemeToggleProps) {
661
+ const { theme, setTheme } = useTheme()
662
+ const [open, setOpen] = React.useState(false)
663
+
664
+ const currentTheme = theme ?? 'system'
665
+
666
+ const triggerIcon = (
667
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24" aria-hidden="true" className="fill-current w-[1em] h-[1em]">
668
+ <path d="M13 20v3h-2v-3h2Zm5.364-3.05 2.121 2.121-1.414 1.414-2.121-2.121 1.414-1.414Zm-12.728 0 1.414 1.414-2.121 2.121-1.414-1.414 2.121-2.121ZM12 6a6 6 0 1 1 0 12 6 6 0 0 1 0-12Zm0 2a4 4 0 0 0-.2 7.995L12 16V8Zm-8 3v2H1v-2h3Zm19 0v2h-3v-2h3ZM4.929 3.515 7.05 5.636 5.636 7.05 3.515 4.93v-.001l1.414-1.414Zm14.142-.001 1.414 1.415-2.121 2.121-1.414-1.414 2.121-2.121v-.001ZM13 1v3h-2V1h2Z" />
669
+ </svg>
670
+ )
671
+
672
+ const triggerLabel = "Param\xE8tres d'affichage"
673
+
674
+ return (
675
+ <Modal open={open} onOpenChange={setOpen}>
676
+ <ModalTrigger asChild>
677
+ <Button
678
+ variant={variant}
679
+ size={size}
680
+ className={className}
681
+ aria-label={iconOnly ? triggerLabel : undefined}
682
+ icon={triggerIcon}...props
683
+ >!iconOnly && triggerLabel
684
+ </Button>
685
+ </ModalTrigger>
686
+
687
+ <ModalContent className="sm:max-w-[480px]">
688
+ <ModalHeader className="border-b-0 pb-0">
689
+ <ModalTitle>Param\xE8tres d'affichage</ModalTitle>
690
+ <ModalDescription>Choisissez un th\xE8me pour personnaliser l'apparence du site.</ModalDescription>
691
+ </ModalHeader>
692
+
693
+ <div className="py-6 px-2">
694
+ <RadioGroup
695
+ value=currentTheme
696
+ onValueChange={(val) => setTheme(val)}
697
+ className="flex flex-col gap-6 justify-center"
698
+ >
699
+ <ThemeOption value="light" label="Th\xE8me clair" Artwork={ThemeArtworkLight} />
700
+ <ThemeOption value="dark" label="Th\xE8me sombre" Artwork={ThemeArtworkDark} />
701
+ <ThemeOption value="system" label="Syst\xE8me" Artwork={ThemeArtworkSystem} hint="Utilise les param\xE8tres syst\xE8me" />
702
+ </RadioGroup>
703
+ </div>
704
+ </ModalContent>
705
+ </Modal>
706
+ )
707
+ }
708
+
709
+ function ThemeOption({
710
+ value,
711
+ label,
712
+ hint,
713
+ Artwork,
714
+ }: {
715
+ value: string
716
+ label: string
717
+ hint?: string
718
+ Artwork: React.ElementType
719
+ }
720
+ )
721
+ {
722
+ const id = React.useId()
723
+
724
+ return (
725
+ <label htmlFor={id}
726
+ className =
727
+ "flex flex-row items-center cursor-pointer border border-border hover:bg-muted focus-within:ring-2 focus-within:ring-ring focus-within:ring-offset-2 transition-colors">
728
+ <div className =
729
+ "flex-1 px-6 py-6 border-r border-border flex items-center h-[96px]">
730
+ <RadioGroupItem value =
731
+ { value }
732
+ id = { id }
733
+ label = { label }
734
+ hint = { hint }
735
+ className="mb-0" />
736
+ </div>
737
+ <div
738
+ className =
739
+ "w-[124px] h-[96px] shrink-0 flex items-center justify-center bg-background-alt overflow-hidden">
740
+ <Artwork className = 'w-20 h-20'
741
+ aria-hidden="true" />
742
+ </div>
743
+ </label>
744
+ )
745
+ }
746
+ `
747
+ };
748
+
749
+ // src/commands/fetch-artworks.ts
750
+ import path from "path";
751
+ import chalk from "chalk";
752
+ import { execa } from "execa";
753
+ import fs from "fs-extra";
754
+ import ora from "ora";
755
+ async function fetchArtworks() {
756
+ console.log(chalk.bold.blue("\n\u{1F1EB}\u{1F1F7} T\xE9l\xE9chargement des Artworks DSFR\n"));
757
+ const spinner = ora("T\xE9l\xE9chargement depuis le d\xE9p\xF4t officiel...").start();
758
+ try {
759
+ const publicDir = path.join(process.cwd(), "public");
760
+ if (!await fs.pathExists(publicDir)) {
761
+ await fs.ensureDir(publicDir);
762
+ }
763
+ const targetDir = path.join(publicDir, "dist", "artwork");
764
+ const tmpDir = path.join(process.cwd(), "node_modules", ".cache", `dsfr-fetch-${Date.now()}`);
765
+ await fs.ensureDir(tmpDir);
766
+ try {
767
+ await execa(
768
+ "git",
769
+ [
770
+ "clone",
771
+ "--depth",
772
+ "1",
773
+ "--filter=blob:none",
774
+ "--sparse",
775
+ "https://github.com/GouvernementFR/dsfr.git",
776
+ "."
777
+ ],
778
+ { cwd: tmpDir }
779
+ );
780
+ await execa("git", ["sparse-checkout", "set", "src/dsfr/core/asset/artwork"], { cwd: tmpDir });
781
+ await fs.ensureDir(targetDir);
782
+ const sourceDir = path.join(tmpDir, "src", "dsfr", "core", "asset", "artwork");
783
+ await fs.copy(sourceDir, targetDir, { overwrite: true });
784
+ spinner.succeed("Artworks t\xE9l\xE9charg\xE9s avec succ\xE8s !");
785
+ console.log(
786
+ chalk.dim(`
787
+ Les fichiers ont \xE9t\xE9 copi\xE9s dans : ${chalk.bold("public/dist/artwork")}`)
788
+ );
789
+ console.log(chalk.dim("Vous pouvez maintenant utiliser les composants comme ThemeToggle.\n"));
790
+ } finally {
791
+ await fs.remove(tmpDir).catch(() => {
792
+ });
793
+ }
794
+ } catch (error) {
795
+ spinner.fail("Erreur lors du t\xE9l\xE9chargement des artworks.");
796
+ console.error(error);
797
+ throw error;
798
+ }
799
+ }
800
+
801
+ // src/commands/add.ts
802
+ var __dirname2 = path2.dirname(fileURLToPath(import.meta.url));
803
+ var AVAILABLE_COMPONENTS = {
804
+ button: {
805
+ name: "Button",
806
+ files: ["button.tsx"],
807
+ dependencies: ["class-variance-authority", "clsx", "tailwind-merge"]
808
+ },
809
+ alert: {
810
+ name: "Alert",
811
+ files: ["alert.tsx"],
812
+ dependencies: ["class-variance-authority", "clsx", "tailwind-merge"]
813
+ },
814
+ card: {
815
+ name: "Card",
816
+ files: ["card.tsx"],
817
+ dependencies: ["class-variance-authority", "clsx", "tailwind-merge"]
818
+ },
819
+ input: {
820
+ name: "Input",
821
+ files: ["input.tsx"],
822
+ dependencies: ["class-variance-authority", "clsx", "tailwind-merge"]
823
+ },
824
+ modal: {
825
+ name: "Modal",
826
+ files: ["modal.tsx"],
827
+ dependencies: ["class-variance-authority", "clsx", "tailwind-merge", "@radix-ui/react-dialog"]
828
+ },
829
+ select: {
830
+ name: "Select",
831
+ files: ["select.tsx"],
832
+ dependencies: ["class-variance-authority", "clsx", "tailwind-merge", "@radix-ui/react-select"]
833
+ },
834
+ themetoggle: {
835
+ name: "ThemeToggle",
836
+ files: ["theme-toggle.tsx", "theme-artwork.tsx"],
837
+ dependencies: ["@radix-ui/react-dialog", "@radix-ui/react-radio-group"]
838
+ }
839
+ };
840
+ async function add(components) {
841
+ console.log(chalk2.bold.blue("\n\u{1F1EB}\u{1F1F7} Ajout de composants DSFR\n"));
842
+ let selectedComponents = [];
843
+ if (!components || components.length === 0) {
844
+ const response = await prompts({
845
+ type: "multiselect",
846
+ name: "components",
847
+ message: "Quels composants voulez-vous ajouter ?",
848
+ choices: Object.entries(AVAILABLE_COMPONENTS).map(([key, value]) => ({
849
+ title: value.name,
850
+ value: key
851
+ }))
852
+ });
853
+ if (!response.components || response.components.length === 0) {
854
+ console.log(chalk2.yellow("\u274C Aucun composant s\xE9lectionn\xE9"));
855
+ process.exit(0);
856
+ }
857
+ selectedComponents = response.components;
858
+ } else {
859
+ selectedComponents = components.filter((c) => {
860
+ if (!(c in AVAILABLE_COMPONENTS)) {
861
+ console.log(chalk2.yellow(`\u26A0\uFE0F Composant inconnu : ${c}`));
862
+ return false;
863
+ }
864
+ return true;
865
+ });
866
+ if (selectedComponents.length === 0) {
867
+ console.log(chalk2.red("\u274C Aucun composant valide"));
868
+ console.log(
869
+ chalk2.dim(`Composants disponibles : ${Object.keys(AVAILABLE_COMPONENTS).join(", ")}`)
870
+ );
871
+ process.exit(1);
872
+ }
873
+ }
874
+ const componentsPath = await detectComponentsPath();
875
+ for (const componentName of selectedComponents) {
876
+ const spinner = ora2(`Ajout de ${AVAILABLE_COMPONENTS[componentName].name}...`).start();
877
+ try {
878
+ await copyComponent(componentName, componentsPath);
879
+ spinner.succeed(`${AVAILABLE_COMPONENTS[componentName].name} ajout\xE9`);
880
+ if (componentName === "themetoggle") {
881
+ spinner.text = "T\xE9l\xE9chargement des Artworks DSFR requis pour ThemeToggle...";
882
+ try {
883
+ await fetchArtworks();
884
+ } catch (_e) {
885
+ spinner.warn(
886
+ "Les artworks n'ont pas pu \xEAtre t\xE9l\xE9charg\xE9s. Ex\xE9cutez `dsfrkit fetch-artworks`."
887
+ );
888
+ }
889
+ }
890
+ } catch (error) {
891
+ spinner.fail(`Erreur lors de l'ajout de ${AVAILABLE_COMPONENTS[componentName].name}`);
892
+ console.error(error);
893
+ }
894
+ }
895
+ console.log(chalk2.green("\n\u2705 Composants ajout\xE9s avec succ\xE8s !\n"));
896
+ console.log(chalk2.dim("Vous pouvez maintenant les utiliser :"));
897
+ for (const name of selectedComponents) {
898
+ console.log(
899
+ chalk2.dim(` import { ${AVAILABLE_COMPONENTS[name].name} } from '@/components/ui/${name}'`)
900
+ );
901
+ }
902
+ console.log();
903
+ }
904
+ async function detectComponentsPath() {
905
+ const possiblePaths = ["src/components/ui", "components/ui", "app/components/ui"];
906
+ for (const p of possiblePaths) {
907
+ if (await fs2.pathExists(path2.join(process.cwd(), p))) {
908
+ return p;
909
+ }
910
+ }
911
+ const defaultPath = "src/components/ui";
912
+ await fs2.ensureDir(path2.join(process.cwd(), defaultPath));
913
+ return defaultPath;
914
+ }
915
+ async function copyComponent(name, targetPath) {
916
+ const component = AVAILABLE_COMPONENTS[name];
917
+ for (const file of component.files) {
918
+ const targetFile = path2.join(process.cwd(), targetPath, file);
919
+ const templateKey = file.replace(".tsx", "");
920
+ const template = componentTemplates[templateKey];
921
+ if (template) {
922
+ await fs2.writeFile(targetFile, template);
923
+ }
924
+ }
925
+ }
926
+
927
+ // src/commands/init.ts
928
+ import path3 from "path";
929
+ import { fileURLToPath as fileURLToPath2 } from "url";
930
+ import chalk3 from "chalk";
931
+ import { execa as execa2 } from "execa";
932
+ import fs3 from "fs-extra";
933
+ import ora3 from "ora";
934
+ import prompts2 from "prompts";
935
+ var __dirname3 = path3.dirname(fileURLToPath2(import.meta.url));
936
+ async function init() {
937
+ console.log(chalk3.bold.blue("\n\u{1F1EB}\u{1F1F7} Initialisation du projet DSFRKit\n"));
938
+ const packageManager = await detectPackageManager();
939
+ const response = await prompts2([
940
+ {
941
+ type: "text",
942
+ name: "componentsPath",
943
+ message: "O\xF9 voulez-vous stocker les composants ?",
944
+ initial: "src/components/ui"
945
+ },
946
+ {
947
+ type: "confirm",
948
+ name: "installDeps",
949
+ message: "Installer les d\xE9pendances n\xE9cessaires ?",
950
+ initial: true
951
+ },
952
+ {
953
+ type: "confirm",
954
+ name: "fetchArtworks",
955
+ message: "T\xE9l\xE9charger les ic\xF4nes et SVG officiels DSFR (recommand\xE9) ?",
956
+ initial: true
957
+ },
958
+ {
959
+ type: "multiselect",
960
+ name: "llmTools",
961
+ message: "Quels outils d'assistance IA utilisez-vous ? (Optionnel)",
962
+ choices: [
963
+ { title: "GitHub Copilot", value: "copilot" },
964
+ { title: "Claude Code", value: "claude" },
965
+ { title: "Cursor", value: "cursor" },
966
+ { title: "Windsurf", value: "windsurf" },
967
+ { title: "OpenAI Codex", value: "codex" },
968
+ { title: "Autre (AGENTS.md)", value: "agents" }
969
+ ],
970
+ instructions: false,
971
+ hint: "- Espace pour s\xE9lectionner. Entr\xE9e pour valider (vide pour ignorer)."
972
+ }
973
+ ]);
974
+ if (!response.componentsPath) {
975
+ console.log(chalk3.yellow("\u274C Initialisation annul\xE9e"));
976
+ process.exit(0);
977
+ }
978
+ const spinner = ora3("Configuration en cours...").start();
979
+ try {
980
+ const componentsDir = path3.join(process.cwd(), response.componentsPath);
981
+ await fs3.ensureDir(componentsDir);
982
+ const libDir = path3.join(process.cwd(), "src/lib");
983
+ await fs3.ensureDir(libDir);
984
+ const utilsContent = `import { type ClassValue, clsx } from 'clsx'
985
+ import { twMerge } from 'tailwind-merge'
986
+
987
+ export function cn(...inputs: ClassValue[]) {
988
+ return twMerge(clsx(inputs))
989
+ }
990
+ `;
991
+ await fs3.writeFile(path3.join(libDir, "utils.ts"), utilsContent);
992
+ await createTailwindConfig();
993
+ spinner.succeed("Configuration cr\xE9\xE9e avec succ\xE8s");
994
+ if (response.installDeps) {
995
+ const depsSpinner = ora3("Installation des d\xE9pendances...").start();
996
+ try {
997
+ await execa2(packageManager, [
998
+ packageManager === "npm" ? "install" : "add",
999
+ "-D",
1000
+ "@dsfrkit/config",
1001
+ "tailwindcss",
1002
+ "class-variance-authority",
1003
+ "clsx",
1004
+ "tailwind-merge"
1005
+ ]);
1006
+ depsSpinner.succeed("D\xE9pendances de d\xE9veloppement install\xE9es");
1007
+ } catch (error) {
1008
+ depsSpinner.fail("Erreur lors de l'installation des d\xE9pendances de d\xE9veloppement");
1009
+ console.error(error);
1010
+ }
1011
+ async function findMonorepoRoot() {
1012
+ let dir = process.cwd();
1013
+ while (true) {
1014
+ if (await fs3.pathExists(path3.join(dir, "pnpm-workspace.yaml"))) return dir;
1015
+ const pkgPath = path3.join(dir, "package.json");
1016
+ if (await fs3.pathExists(pkgPath)) {
1017
+ try {
1018
+ const pkg = await fs3.readJSON(pkgPath);
1019
+ if (pkg.workspaces) return dir;
1020
+ } catch {
1021
+ }
1022
+ }
1023
+ const parent = path3.dirname(dir);
1024
+ if (parent === dir) break;
1025
+ dir = parent;
1026
+ }
1027
+ return null;
1028
+ }
1029
+ const monorepoRoot = await findMonorepoRoot();
1030
+ const tokensSpecifier = packageManager === "pnpm" && monorepoRoot ? "@dsfrkit/tokens@workspace:*" : "@dsfrkit/tokens";
1031
+ const tokensSpinner = ora3(`Installation de ${tokensSpecifier}...`).start();
1032
+ try {
1033
+ await execa2(packageManager, [packageManager === "npm" ? "install" : "add", tokensSpecifier]);
1034
+ tokensSpinner.succeed(`${tokensSpecifier} install\xE9`);
1035
+ } catch (error) {
1036
+ tokensSpinner.fail("Erreur lors de l'installation de @dsfrkit/tokens");
1037
+ console.error(error);
1038
+ }
1039
+ }
1040
+ if (response.fetchArtworks) {
1041
+ try {
1042
+ await fetchArtworks();
1043
+ } catch (_e) {
1044
+ console.log(
1045
+ chalk3.yellow(
1046
+ "\n\u26A0\uFE0F Le t\xE9l\xE9chargement des artworks a \xE9chou\xE9. Vous pourrez r\xE9essayer avec la commande: dsfrkit fetch-artworks\n"
1047
+ )
1048
+ );
1049
+ }
1050
+ }
1051
+ if (response.llmTools && response.llmTools.length > 0) {
1052
+ const llmSpinner = ora3("Cr\xE9ation des instructions IA...").start();
1053
+ try {
1054
+ const aiRulesContent = `# R\xC8GLES DSFRKIT POUR LES IA
1055
+ Tu utilises la librairie @dsfrkit/react. N'\xE9cris JAMAIS de composants d'UI en HTML brut ou classes Tailwind.
1056
+ Pour la documentation compl\xE8te et \xE0 jour de tous les composants (props, exemples, layouts), r\xE9f\xE8re-toi toujours \xE0 ce fichier :
1057
+ https://ra-nouvelle-aquitaine.github.io/dsfrkit/llms.txt
1058
+ `;
1059
+ const tools = response.llmTools;
1060
+ for (const tool of tools) {
1061
+ if (tool === "copilot") {
1062
+ const githubDir = path3.join(process.cwd(), ".github");
1063
+ await fs3.ensureDir(githubDir);
1064
+ await fs3.writeFile(path3.join(githubDir, "copilot-instructions.md"), aiRulesContent);
1065
+ } else if (tool === "claude") {
1066
+ await fs3.writeFile(path3.join(process.cwd(), "clauderc.md"), aiRulesContent);
1067
+ } else if (tool === "cursor") {
1068
+ await fs3.writeFile(path3.join(process.cwd(), ".cursorrules"), aiRulesContent);
1069
+ } else if (tool === "windsurf") {
1070
+ await fs3.writeFile(path3.join(process.cwd(), ".windsurfrules"), aiRulesContent);
1071
+ } else if (tool === "agents" || tool === "codex") {
1072
+ await fs3.writeFile(path3.join(process.cwd(), "AGENTS.md"), aiRulesContent);
1073
+ }
1074
+ }
1075
+ const list = tools.join(", ");
1076
+ llmSpinner.succeed(`Fichiers d'instructions IA g\xE9n\xE9r\xE9s pour : ${list}`);
1077
+ } catch (_error) {
1078
+ llmSpinner.fail("Erreur lors de la cr\xE9ation des fichiers d'instructions IA");
1079
+ }
1080
+ }
1081
+ console.log(chalk3.green("\n\u2705 Projet initialis\xE9 avec succ\xE8s !\n"));
1082
+ console.log(chalk3.dim("Prochaines \xE9tapes :"));
1083
+ console.log(chalk3.dim(` 1. V\xE9rifiez votre ${chalk3.bold("tailwind.config.js")}`));
1084
+ console.log(
1085
+ chalk3.dim(` 2. Ajoutez des composants : ${chalk3.bold("dsfrkit add button alert")}`)
1086
+ );
1087
+ console.log();
1088
+ } catch (error) {
1089
+ spinner.fail("Erreur lors de la configuration");
1090
+ console.error(error);
1091
+ process.exit(1);
1092
+ }
1093
+ }
1094
+ async function detectPackageManager() {
1095
+ if (await fs3.pathExists("pnpm-lock.yaml")) return "pnpm";
1096
+ if (await fs3.pathExists("yarn.lock")) return "yarn";
1097
+ if (await fs3.pathExists("bun.lockb")) return "bun";
1098
+ return "npm";
1099
+ }
1100
+ async function createTailwindConfig() {
1101
+ const configPath = path3.join(process.cwd(), "tailwind.config.js");
1102
+ const configContent = `import dsfrPreset from '@dsfrkit/config'
1103
+
1104
+ /** @type {import('tailwindcss').Config} */
1105
+ export default {
1106
+ presets: [dsfrPreset],
1107
+ content: [
1108
+ './index.html',
1109
+ './src/**/*.{js,ts,jsx,tsx}',
1110
+ ],
1111
+ theme: {
1112
+ extend: {},
1113
+ },
1114
+ plugins: [],
1115
+ }
1116
+ `;
1117
+ if (await fs3.pathExists(configPath)) {
1118
+ console.log(chalk3.yellow("\n\u26A0\uFE0F tailwind.config.js existe d\xE9j\xE0"));
1119
+ console.log(chalk3.dim("Ajoutez manuellement le preset :"));
1120
+ console.log(chalk3.dim(" presets: [dsfrPreset]"));
1121
+ } else {
1122
+ await fs3.writeFile(configPath, configContent);
1123
+ }
1124
+ }
1125
+
1126
+ // src/index.ts
1127
+ var program = new Command();
1128
+ program.name("dsfrkit").description("CLI pour le Syst\xE8me de Design de l'\xC9tat fran\xE7ais (DSFR)").version("0.1.0");
1129
+ program.command("init").description("Initialiser le projet avec la configuration DSFR").action(init);
1130
+ program.command("add").description("Ajouter des composants au projet").argument("[components...]", "Composants \xE0 ajouter (ex: button alert)").action(add);
1131
+ program.command("fetch-artworks").description("T\xE9l\xE9charge les SVG officiels DSFR dans le dossier public/dist/artwork").action(fetchArtworks);
1132
+ program.parse();
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "@dsfrkit/cli",
3
+ "version": "0.1.0",
4
+ "description": "CLI pour installer et copier les composants DSFR",
5
+ "bin": {
6
+ "dsfrkit": "./dist/index.js"
7
+ },
8
+ "main": "./dist/index.js",
9
+ "type": "module",
10
+ "files": [
11
+ "dist",
12
+ "templates"
13
+ ],
14
+ "scripts": {
15
+ "build": "tsup",
16
+ "dev": "tsup --watch",
17
+ "test": "vitest run",
18
+ "test:watch": "vitest",
19
+ "typecheck": "tsc --noEmit",
20
+ "clean": "rm -rf dist"
21
+ },
22
+ "keywords": [
23
+ "dsfr",
24
+ "cli",
25
+ "components",
26
+ "tailwindcss"
27
+ ],
28
+ "license": "ETALAB-2.0",
29
+ "publishConfig": {
30
+ "access": "public"
31
+ },
32
+ "dependencies": {
33
+ "chalk": "^5.3.0",
34
+ "commander": "^12.1.0",
35
+ "execa": "^9.5.2",
36
+ "fs-extra": "^11.2.0",
37
+ "ora": "^8.1.1",
38
+ "prompts": "^2.4.2"
39
+ },
40
+ "devDependencies": {
41
+ "@types/fs-extra": "^11.0.4",
42
+ "@types/prompts": "^2.4.9",
43
+ "tsup": "^8.3.5",
44
+ "typescript": "^5.7.2"
45
+ }
46
+ }