@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.
- package/README.md +90 -0
- package/dist/index.js +1132 -0
- 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
|
+
}
|