Cours Next.js · Module 3: Styles et optimisation visuelle
Shadcn/UI - Composants pour Next.js 16
Dans cette leçon9
À Metropolis Quantum, des laboratoires de recherche avancés ont mis au point des éléments de construction modulaires et préfabriqués qui se combinent et se personnalisent facilement selon les besoins. Grâce à cela, les architectes peuvent rapidement créer des bâtiments à l'esthétique cohérente tout en gardant la flexibilité de concevoir des espaces uniques. Un rôle similaire dans le monde du développement frontend est joué par la bibliothèque Shadcn/UI : une collection de composants d'interface utilisateur soigneusement conçus et accessibles, faciles à intégrer et à personnaliser selon les besoins du projet.
Qu'est-ce que Shadcn/UI ?
Shadcn/UI est une collection de composants d'interface conçus avec Radix UI, Tailwind CSS et React à l'esprit. Contrairement aux bibliothèques de composants traditionnelles, Shadcn/UI ne s'installe pas comme une dépendance npm. À la place, les composants sont copiés directement dans ton projet, ce qui te donne un contrôle total sur le code et permet des modifications faciles selon les besoins.
Caractéristiques clés de Shadcn/UI :
- Aucune dépendance à une bibliothèque externe - les composants font partie de ton code
- Un contrôle total sur les styles - une modification facile de l'apparence des composants
- Construit sur Radix UI - des primitives solides et accessibles pour les composants interactifs
- Stylisé avec Tailwind CSS - un design system cohérent et facile à personnaliser
- Mode sombre par défaut - une prise en charge intégrée des thèmes clair et sombre
- Un typage TypeScript strict - une prise en charge complète des types
- La modularité - n'installe que les composants dont tu as besoin
- Le responsive - les composants s'adaptent à différentes tailles d'écran
Installation et configuration de Shadcn/UI dans Next.js 16
Pour démarrer avec Shadcn/UI dans un projet Next.js 16, tu dois suivre quelques étapes :
Étape 1 : Installer le CLI de Shadcn/UI
1npx shadcn@latest initAprès avoir exécuté cette commande, le CLI détecte le projet Next.js, enregistre les réglages dans le fichier components.json et pose quelques questions. Les anciennes versions de l'outil, publiées sous le nom de paquet shadcn-ui, demandaient notamment :
- Le choix du style - généralement « Default » est un bon choix
- Le choix du chemin des composants - typiquement « src/components/ui »
- Le choix du jeu de couleurs - tu peux choisir par défaut ou personnalisé
- Veux-tu activer le mode sombre ? - généralement « oui »
- Le choix du préfixe des composants - typiquement aucun préfixe
- Le chemin du fichier globals.css - typiquement « app/globals.css »
- Placer les variables CSS dans globals.css ? - généralement « oui »
Étape 2 : Vérifier la configuration de Tailwind CSS
Dans un projet avec Tailwind CSS 4, qu'installe create-next-app dans Next.js 16, le CLI enregistre les couleurs et les arrondis sous forme de variables CSS dans globals.css (dans un bloc @theme inline) et ne crée pas de fichier tailwind.config.js. Dans les projets avec Tailwind CSS 3, le CLI met justement à jour ce fichier - il vaut alors la peine de vérifier son contenu :
1// tailwind.config.js
2/** @type {import('tailwindcss').Config} */
3module.exports = {
4 darkMode: ["class"],
5 content: [
6 './pages/**/*.{js,ts,jsx,tsx,mdx}',
7 './components/**/*.{js,ts,jsx,tsx,mdx}',
8 './app/**/*.{js,ts,jsx,tsx,mdx}',
9 ],
10 theme: {
11 container: {
12 center: true,
13 padding: "2rem",
14 screens: {
15 "2xl": "1400px",
16 },
17 },
18 extend: {
19 colors: {
20 border: "hsl(var(--border))",
21 input: "hsl(var(--input))",
22 ring: "hsl(var(--ring))",
23 background: "hsl(var(--background))",
24 foreground: "hsl(var(--foreground))",
25 primary: {
26 DEFAULT: "hsl(var(--primary))",
27 foreground: "hsl(var(--primary-foreground))",
28 },
29 secondary: {
30 DEFAULT: "hsl(var(--secondary))",
31 foreground: "hsl(var(--secondary-foreground))",
32 },
33 destructive: {
34 DEFAULT: "hsl(var(--destructive))",
35 foreground: "hsl(var(--destructive-foreground))",
36 },
37 muted: {
38 DEFAULT: "hsl(var(--muted))",
39 foreground: "hsl(var(--muted-foreground))",
40 },
41 accent: {
42 DEFAULT: "hsl(var(--accent))",
43 foreground: "hsl(var(--accent-foreground))",
44 },
45 popover: {
46 DEFAULT: "hsl(var(--popover))",
47 foreground: "hsl(var(--popover-foreground))",
48 },
49 card: {
50 DEFAULT: "hsl(var(--card))",
51 foreground: "hsl(var(--card-foreground))",
52 },
53 },
54 borderRadius: {
55 lg: "var(--radius)",
56 md: "calc(var(--radius) - 2px)",
57 sm: "calc(var(--radius) - 4px)",
58 },
59 keyframes: {
60 "accordion-down": {
61 from: { height: 0 },
62 to: { height: "var(--radix-accordion-content-height)" },
63 },
64 "accordion-up": {
65 from: { height: "var(--radix-accordion-content-height)" },
66 to: { height: 0 },
67 },
68 },
69 animation: {
70 "accordion-down": "accordion-down 0.2s ease-out",
71 "accordion-up": "accordion-up 0.2s ease-out",
72 },
73 },
74 },
75 plugins: [require("tailwindcss-animate")],
76}Étape 3 : Vérifier les variables CSS
Le CLI ajoutera aussi des variables CSS au fichier globals.css :
1@tailwind base;
2@tailwind components;
3@tailwind utilities;
4
5@layer base {
6 :root {
7 --background: 0 0% 100%;
8 --foreground: 222.2 84% 4.9%;
9
10 --card: 0 0% 100%;
11 --card-foreground: 222.2 84% 4.9%;
12
13 --popover: 0 0% 100%;
14 --popover-foreground: 222.2 84% 4.9%;
15
16 --primary: 222.2 47.4% 11.2%;
17 --primary-foreground: 210 40% 98%;
18
19 --secondary: 210 40% 96.1%;
20 --secondary-foreground: 222.2 47.4% 11.2%;
21
22 --muted: 210 40% 96.1%;
23 --muted-foreground: 215.4 16.3% 46.9%;
24
25 --accent: 210 40% 96.1%;
26 --accent-foreground: 222.2 47.4% 11.2%;
27
28 --destructive: 0 84.2% 60.2%;
29 --destructive-foreground: 210 40% 98%;
30
31 --border: 214.3 31.8% 91.4%;
32 --input: 214.3 31.8% 91.4%;
33 --ring: 222.2 84% 4.9%;
34
35 --radius: 0.5rem;
36 }
37
38 .dark {
39 --background: 222.2 84% 4.9%;
40 --foreground: 210 40% 98%;
41
42 --card: 222.2 84% 4.9%;
43 --card-foreground: 210 40% 98%;
44
45 --popover: 222.2 84% 4.9%;
46 --popover-foreground: 210 40% 98%;
47
48 --primary: 210 40% 98%;
49 --primary-foreground: 222.2 47.4% 11.2%;
50
51 --secondary: 217.2 32.6% 17.5%;
52 --secondary-foreground: 210 40% 98%;
53
54 --muted: 217.2 32.6% 17.5%;
55 --muted-foreground: 215 20.2% 65.1%;
56
57 --accent: 217.2 32.6% 17.5%;
58 --accent-foreground: 210 40% 98%;
59
60 --destructive: 0 62.8% 30.6%;
61 --destructive-foreground: 210 40% 98%;
62
63 --border: 217.2 32.6% 17.5%;
64 --input: 217.2 32.6% 17.5%;
65 --ring: 212.7 26.8% 83.9%;
66 }
67}
68
69@layer base {
70 * {
71 @apply border-border;
72 }
73 body {
74 @apply bg-background text-foreground;
75 }
76}Étape 4 : Ajouter des composants Shadcn/UI
Tu peux maintenant installer les composants choisis à l'aide du CLI :
1npx shadcn@latest add buttonCette commande ajoutera le composant Button au répertoire components/ui :
1// components/ui/button.tsx
2import * as React from "react"
3import { Slot } from "@radix-ui/react-slot"
4import { cva, type VariantProps } from "class-variance-authority"
5
6import { cn } from "@/lib/utils"
7
8const buttonVariants = cva(
9 "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
10 {
11 variants: {
12 variant: {
13 default: "bg-primary text-primary-foreground hover:bg-primary/90",
14 destructive:
15 "bg-destructive text-destructive-foreground hover:bg-destructive/90",
16 outline:
17 "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
18 secondary:
19 "bg-secondary text-secondary-foreground hover:bg-secondary/80",
20 ghost: "hover:bg-accent hover:text-accent-foreground",
21 link: "text-primary underline-offset-4 hover:underline",
22 },
23 size: {
24 default: "h-10 px-4 py-2",
25 sm: "h-9 rounded-md px-3",
26 lg: "h-11 rounded-md px-8",
27 icon: "h-10 w-10",
28 },
29 },
30 defaultVariants: {
31 variant: "default",
32 size: "default",
33 },
34 }
35)
36
37export interface ButtonProps
38 extends React.ButtonHTMLAttributes<HTMLButtonElement>,
39 VariantProps<typeof buttonVariants> {
40 asChild?: boolean
41}
42
43const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
44 ({ className, variant, size, asChild = false, ...props }, ref) => {
45 const Comp = asChild ? Slot : "button"
46 return (
47 <Comp
48 className={cn(buttonVariants({ variant, size, className }))}
49 ref={ref}
50 {...props}
51 />
52 )
53 }
54)
55Button.displayName = "Button"
56
57export { Button, buttonVariants }Le CLI ajoutera aussi automatiquement un fichier lib/utils.ts contenant des fonctions utilitaires :
1// lib/utils.ts
2import { type ClassValue, clsx } from "clsx"
3import { twMerge } from "tailwind-merge"
4
5export function cn(...inputs: ClassValue[]) {
6 return twMerge(clsx(inputs))
7}Principaux composants Shadcn/UI
Shadcn/UI propose de nombreux composants, des simples boutons aux composants d'interaction avancés. En voici quelques-uns parmi les plus importants :
Composants de base
- Button - des boutons avec différentes variantes
- Input - des champs de saisie
- Checkbox - des cases à cocher
- RadioGroup - des groupes d'options radio
- Select - des listes déroulantes
- Textarea - des champs de texte multilignes
- Label - des étiquettes pour les contrôles de formulaire
Composants de mise en page
- Card - des cards pour regrouper du contenu
- AspectRatio - le maintien des proportions des éléments
- ScrollArea - des zones de défilement
- Separator - des lignes de séparation
- Sheet - des panneaux latéraux coulissants
Composants de navigation
- Tabs - des onglets
- Navigation Menu - un menu de navigation
- Breadcrumb - un fil d'Ariane de navigation
- Pagination - la pagination
Composants de dialogue
- Dialog - des fenêtres de dialogue
- Alert Dialog - des alertes nécessitant une confirmation
- Drawer - des panneaux coulissants
- Popover - des bulles contextuelles
- Toast - des notifications
Composants de données
- Table - des tableaux
- Accordion - des sections dépliables
- Calendar - des calendriers
- Carousel - des carrousels
Composants de formulaire
- Form - une solution de formulaire complète
- Combobox - des champs de sélection avancés avec recherche
- Slider - des curseurs
- Switch - des interrupteurs
Exemples d'utilisation de Shadcn/UI dans un projet Next.js 16
Exemple 1 : formulaire de connexion
1// app/login/page.tsx
2"use client"
3
4import { useState } from "react"
5import { useRouter } from "next/navigation"
6import { zodResolver } from "@hookform/resolvers/zod"
7import { useForm } from "react-hook-form"
8import * as z from "zod"
9
10import { Button } from "@/components/ui/button"
11import {
12 Form,
13 FormControl,
14 FormField,
15 FormItem,
16 FormLabel,
17 FormMessage,
18} from "@/components/ui/form"
19import { Input } from "@/components/ui/input"
20import { toast } from "@/components/ui/use-toast"
21
22const formSchema = z.object({
23 email: z.string().email({
24 message: "Please enter a valid email address.",
25 }),
26 password: z.string().min(8, {
27 message: "Password must contain at least 8 characters.",
28 }),
29})
30
31export default function LoginPage() {
32 const router = useRouter()
33 const [isLoading, setIsLoading] = useState(false)
34
35 const form = useForm<z.infer<typeof formSchema>>({
36 resolver: zodResolver(formSchema),
37 defaultValues: {
38 email: "",
39 password: "",
40 },
41 })
42
43 async function onSubmit(values: z.infer<typeof formSchema>) {
44 setIsLoading(true)
45
46 try {
47 // In a real application, we would call the login API here
48 console.log(values)
49
50 // Simulating delay
51 await new Promise(resolve => setTimeout(resolve, 1000))
52
53 toast({
54 title: "Logged in successfully",
55 description: "Redirecting you to the main dashboard",
56 })
57
58 router.push("/dashboard")
59 } catch (error) {
60 toast({
61 variant: "destructive",
62 title: "An error occurred",
63 description: "Unable to log in. Check your credentials and try again.",
64 })
65 } finally {
66 setIsLoading(false)
67 }
68 }
69
70 return (
71 <div className="mx-auto max-w-md space-y-6 p-6">
72 <div className="space-y-2 text-center">
73 <h1 className="text-3xl font-bold">Logowanie</h1>
74 <p className="text-gray-500 dark:text-gray-400">
75 Enter your credentials to log in to the Metropolis Quantum panel
76 </p>
77 </div>
78 <Form {...form}>
79 <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
80 <FormField
81 control={form.control}
82 name="email"
83 render={({ field }) => (
84 <FormItem>
85 <FormLabel>Email</FormLabel>
86 <FormControl>
87 <Input type="email" placeholder="email@example.com" {...field} />
88 </FormControl>
89 <FormMessage />
90 </FormItem>
91 )}
92 />
93 <FormField
94 control={form.control}
95 name="password"
96 render={({ field }) => (
97 <FormItem>
98 <FormLabel>Password</FormLabel>
99 <FormControl>
100 <Input type="password" {...field} />
101 </FormControl>
102 <FormMessage />
103 </FormItem>
104 )}
105 />
106 <Button type="submit" className="w-full" disabled={isLoading}>
107 {isLoading ? "Logging in..." : "Log in"}
108 </Button>
109 </form>
110 </Form>
111 <div className="text-center text-sm">
112 <a href="/forgot-password" className="text-primary hover:underline">
113 Forgot your password?
114 </a>
115 </div>
116 </div>
117 )
118}Exemple 2 : panneau avec onglets
1// app/dashboard/page.tsx
2"use client"
3
4import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
5import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"
6import { Button } from "@/components/ui/button"
7
8export default function DashboardPage() {
9 return (
10 <div className="container mx-auto py-10">
11 <h1 className="text-3xl font-bold mb-6">Dashboard Metropolis Quantum</h1>
12
13 <Tabs defaultValue="overview" className="space-y-4">
14 <TabsList className="grid w-full grid-cols-4">
15 <TabsTrigger value="overview">Overview</TabsTrigger>
16 <TabsTrigger value="analytics">Analityka</TabsTrigger>
17 <TabsTrigger value="reports">Raporty</TabsTrigger>
18 <TabsTrigger value="settings">Ustawienia</TabsTrigger>
19 </TabsList>
20
21 <TabsContent value="overview" className="space-y-4">
22 <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
23 <Card>
24 <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
25 <CardTitle className="text-sm font-medium">Total Revenue</CardTitle>
26 <svg
27 xmlns="http://www.w3.org/2000/svg"
28 viewBox="0 0 24 24"
29 fill="none"
30 stroke="currentColor"
31 strokeLinecap="round"
32 strokeLinejoin="round"
33 strokeWidth="2"
34 className="h-4 w-4 text-muted-foreground"
35 >
36 <path d="M12 2v20M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6" />
37 </svg>
38 </CardHeader>
39 <CardContent>
40 <div className="text-2xl font-bold">$45,231.89</div>
41 <p className="text-xs text-muted-foreground">
42 +20.1% from last month
43 </p>
44 </CardContent>
45 </Card>
46 <Card>
47 <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
48 <CardTitle className="text-sm font-medium">Subskrypcje</CardTitle>
49 <svg
50 xmlns="http://www.w3.org/2000/svg"
51 viewBox="0 0 24 24"
52 fill="none"
53 stroke="currentColor"
54 strokeLinecap="round"
55 strokeLinejoin="round"
56 strokeWidth="2"
57 className="h-4 w-4 text-muted-foreground"
58 >
59 <path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2" />
60 <circle cx="9" cy="7" r="4" />
61 <path d="M22 21v-2a4 4 0 0 0-3-3.87M16 3.13a4 4 0 0 1 0 7.75" />
62 </svg>
63 </CardHeader>
64 <CardContent>
65 <div className="text-2xl font-bold">+2350</div>
66 <p className="text-xs text-muted-foreground">
67 +180.1% from last month
68 </p>
69 </CardContent>
70 </Card>
71 <Card>
72 <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
73 <CardTitle className="text-sm font-medium">Sales</CardTitle>
74 <svg
75 xmlns="http://www.w3.org/2000/svg"
76 viewBox="0 0 24 24"
77 fill="none"
78 stroke="currentColor"
79 strokeLinecap="round"
80 strokeLinejoin="round"
81 strokeWidth="2"
82 className="h-4 w-4 text-muted-foreground"
83 >
84 <rect width="20" height="14" x="2" y="5" rx="2" />
85 <path d="M2 10h20" />
86 </svg>
87 </CardHeader>
88 <CardContent>
89 <div className="text-2xl font-bold">+12,234</div>
90 <p className="text-xs text-muted-foreground">
91 +19% from last month
92 </p>
93 </CardContent>
94 </Card>
95 <Card>
96 <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
97 <CardTitle className="text-sm font-medium">Active Users</CardTitle>
98 <svg
99 xmlns="http://www.w3.org/2000/svg"
100 viewBox="0 0 24 24"
101 fill="none"
102 stroke="currentColor"
103 strokeLinecap="round"
104 strokeLinejoin="round"
105 strokeWidth="2"
106 className="h-4 w-4 text-muted-foreground"
107 >
108 <path d="M22 12h-4l-3 9L9 3l-3 9H2" />
109 </svg>
110 </CardHeader>
111 <CardContent>
112 <div className="text-2xl font-bold">+573</div>
113 <p className="text-xs text-muted-foreground">
114 +201 from last month
115 </p>
116 </CardContent>
117 </Card>
118 </div>
119 <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-7">
120 <Card className="col-span-4">
121 <CardHeader>
122 <CardTitle>Overview</CardTitle>
123 </CardHeader>
124 <CardContent className="pl-2">
125 {/* A chart could go here */}
126 <div className="h-[200px] bg-muted rounded-md flex items-center justify-center">
127 Revenue Chart
128 </div>
129 </CardContent>
130 </Card>
131 <Card className="col-span-3">
132 <CardHeader>
133 <CardTitle>Ostatnie Transakcje</CardTitle>
134 <CardDescription>
135 24 transactions completed this month.
136 </CardDescription>
137 </CardHeader>
138 <CardContent>
139 <div className="space-y-8">
140 {/* Example transaction */}
141 <div className="flex items-center">
142 <div className="mr-4 rounded-full bg-primary/10 p-2">
143 <svg
144 xmlns="http://www.w3.org/2000/svg"
145 viewBox="0 0 24 24"
146 fill="none"
147 stroke="currentColor"
148 strokeLinecap="round"
149 strokeLinejoin="round"
150 strokeWidth="2"
151 className="h-4 w-4 text-primary"
152 >
153 <path d="M12 2v20M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6" />
154 </svg>
155 </div>
156 <div className="space-y-1">
157 <p className="text-sm font-medium">Quantum Plan (Roczny)</p>
158 <p className="text-xs text-muted-foreground">14 kwietnia 2024</p>
159 </div>
160 <div className="ml-auto font-medium">+$4,999.00</div>
161 </div>
162 {/* More transactions */}
163 </div>
164 </CardContent>
165 </Card>
166 </div>
167 </TabsContent>
168
169 <TabsContent value="analytics" className="space-y-4">
170 <Card className="col-span-4">
171 <CardHeader>
172 <CardTitle>Analityka</CardTitle>
173 <CardDescription>
174 Detailed traffic and conversion analysis.
175 </CardDescription>
176 </CardHeader>
177 <CardContent>
178 <div className="h-[300px] bg-muted rounded-md flex items-center justify-center">
179 Données analytiques
180 </div>
181 </CardContent>
182 </Card>
183 </TabsContent>
184
185 <TabsContent value="reports" className="space-y-4">
186 <Card>
187 <CardHeader>
188 <CardTitle>Raporty</CardTitle>
189 <CardDescription>
190 Browse and generate activity reports.
191 </CardDescription>
192 </CardHeader>
193 <CardContent className="space-y-4">
194 <div className="space-y-2">
195 <div className="flex items-center justify-between">
196 <div>
197 <p className="text-sm font-medium">Monthly Report (April 2024)</p>
198 <p className="text-xs text-muted-foreground">Créé le 01.05.2024</p>
199 </div>
200 <Button variant="outline" size="sm">Télécharger</Button>
201 </div>
202 <div className="flex items-center justify-between">
203 <div>
204 <p className="text-sm font-medium">Raport Kwartalny (Q1 2024)</p>
205 <p className="text-xs text-muted-foreground">Créé le 05.04.2024</p>
206 </div>
207 <Button variant="outline" size="sm">Télécharger</Button>
208 </div>
209 </div>
210 </CardContent>
211 <CardFooter>
212 <Button>Generuj Nowy Raport</Button>
213 </CardFooter>
214 </Card>
215 </TabsContent>
216
217 <TabsContent value="settings" className="space-y-4">
218 <Card>
219 <CardHeader>
220 <CardTitle>Ustawienia</CardTitle>
221 <CardDescription>
222 Manage your account settings.
223 </CardDescription>
224 </CardHeader>
225 <CardContent className="space-y-4">
226 {/* Example settings */}
227 <div className="space-y-1">
228 <p className="text-sm font-medium">Powiadomienia Email</p>
229 <p className="text-xs text-muted-foreground">Receive notifications about important updates.</p>
230 </div>
231 {/* More settings */}
232 </CardContent>
233 <CardFooter>
234 <Button>Enregistrer les modifications</Button>
235 </CardFooter>
236 </Card>
237 </TabsContent>
238 </Tabs>
239 </div>
240 )
241}Exemple 3 : dialogue avec formulaire
1// components/quantum-project-dialog.tsx
2"use client"
3
4import { useState } from "react"
5import { Button } from "@/components/ui/button"
6import {
7 Dialog,
8 DialogContent,
9 DialogDescription,
10 DialogFooter,
11 DialogHeader,
12 DialogTitle,
13 DialogTrigger,
14} from "@/components/ui/dialog"
15import { Input } from "@/components/ui/input"
16import { Label } from "@/components/ui/label"
17import { Textarea } from "@/components/ui/textarea"
18import {
19 Select,
20 SelectContent,
21 SelectItem,
22 SelectTrigger,
23 SelectValue,
24} from "@/components/ui/select"
25import { toast } from "@/components/ui/use-toast"
26
27export function QuantumProjectDialog() {
28 const [open, setOpen] = useState(false)
29 const [isLoading, setIsLoading] = useState(false)
30
31 const [formData, setFormData] = useState({
32 name: "",
33 description: "",
34 category: "",
35 priority: "",
36 })
37
38 const handleChange = (field: string, value: string) => {
39 setFormData((prev) => ({ ...prev, [field]: value }))
40 }
41
42 const handleSubmit = async (e: React.FormEvent) => {
43 e.preventDefault()
44 setIsLoading(true)
45
46 // Symulacja zapisywania danych
47 await new Promise(resolve => setTimeout(resolve, 1000))
48
49 toast({
50 title: "Projet créé",
51 description: `Project "${formData.name}" has been successfully created.`,
52 })
53
54 setIsLoading(false)
55 setOpen(false)
56 // Reset formularza
57 setFormData({
58 name: "",
59 description: "",
60 category: "",
61 priority: "",
62 })
63 }
64
65 return (
66 <Dialog open={open} onOpenChange={setOpen}>
67 <DialogTrigger asChild>
68 <Button variant="default">Nowy Projekt</Button>
69 </DialogTrigger>
70 <DialogContent className="sm:max-w-[425px]">
71 <form onSubmit={handleSubmit}>
72 <DialogHeader>
73 <DialogTitle>Create New Project</DialogTitle>
74 <DialogDescription>
75 Enter project details. Click save when you are done.
76 </DialogDescription>
77 </DialogHeader>
78 <div className="grid gap-4 py-4">
79 <div className="grid gap-2">
80 <Label htmlFor="name">Nom du projet</Label>
81 <Input
82 id="name"
83 value={formData.name}
84 onChange={(e) => handleChange("name", e.target.value)}
85 placeholder="Nom du projet"
86 required
87 />
88 </div>
89 <div className="grid gap-2">
90 <Label htmlFor="description">Opis</Label>
91 <Textarea
92 id="description"
93 value={formData.description}
94 onChange={(e) => handleChange("description", e.target.value)}
95 placeholder="Describe your project"
96 required
97 />
98 </div>
99 <div className="grid grid-cols-2 gap-4">
100 <div className="grid gap-2">
101 <Label htmlFor="category">Kategoria</Label>
102 <Select
103 value={formData.category}
104 onValueChange={(value) => handleChange("category", value)}
105 required
106 >
107 <SelectTrigger id="category">
108 <SelectValue placeholder="Choose a category" />
109 </SelectTrigger>
110 <SelectContent>
111 <SelectItem value="development">Development</SelectItem>
112 <SelectItem value="research">Recherche</SelectItem>
113 <SelectItem value="infrastructure">Infrastruktura</SelectItem>
114 <SelectItem value="marketing">Marketing</SelectItem>
115 <SelectItem value="other">Inne</SelectItem>
116 </SelectContent>
117 </Select>
118 </div>
119 <div className="grid gap-2">
120 <Label htmlFor="priority">Priorytet</Label>
121 <Select
122 value={formData.priority}
123 onValueChange={(value) => handleChange("priority", value)}
124 required
125 >
126 <SelectTrigger id="priority">
127 <SelectValue placeholder="Choisir la priorité" />
128 </SelectTrigger>
129 <SelectContent>
130 <SelectItem value="low">Faible</SelectItem>
131 <SelectItem value="medium">Medium</SelectItem>
132 <SelectItem value="high">Wysoki</SelectItem>
133 <SelectItem value="critical">Krytyczny</SelectItem>
134 </SelectContent>
135 </Select>
136 </div>
137 </div>
138 </div>
139 <DialogFooter>
140 <Button type="button" variant="outline" onClick={() => setOpen(false)}>
141 Cancel
142 </Button>
143 <Button type="submit" disabled={isLoading}>
144 {isLoading ? "Zapisywanie..." : "Save projekt"}
145 </Button>
146 </DialogFooter>
147 </form>
148 </DialogContent>
149 </Dialog>
150 )
151}Personnaliser les composants Shadcn/UI
L'un des plus grands avantages de Shadcn/UI est la facilité de personnalisation des composants. Comme les composants font partie de ton code, tu peux les modifier selon tes besoins.
Personnaliser les styles
Tu peux modifier l'apparence des composants de plusieurs façons :
- Changer les variables CSS - mets à jour les valeurs des variables dans
globals.css
1:root {
2 --primary: 240 5.9% 10%; /* Changement de couleur primary */
3 --radius: 0.75rem; /* Changing corner rounding */
4}- Modifier la définition du composant - édite directement le fichier du composant
1// components/ui/button.tsx
2const buttonVariants = cva(
3 "inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:opacity-50 disabled:pointer-events-none ring-offset-background",
4 {
5 variants: {
6 variant: {
7 default: "bg-primary text-primary-foreground hover:bg-primary/90",
8 destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
9 outline: "border border-input hover:bg-accent hover:text-accent-foreground",
10 secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
11 ghost: "hover:bg-accent hover:text-accent-foreground",
12 link: "underline-offset-4 hover:underline text-primary",
13 // Nous ajoutons une nouvelle variante
14 gradient: "bg-gradient-to-r from-blue-500 to-purple-500 text-white hover:opacity-90",
15 },
16 // Other definitions
17 },
18 }
19)
20
21// Update the interface to include the new variant
22export interface ButtonProps
23 extends React.ButtonHTMLAttributes<HTMLButtonElement>,
24 VariantProps<typeof buttonVariants> {
25 asChild?: boolean
26}- Étendre les composants - crée tes propres composants basés sur Shadcn/UI
1// components/quantum/quantum-card.tsx
2import { Card, CardContent, CardFooter, CardHeader } from "@/components/ui/card"
3import { cn } from "@/lib/utils"
4
5interface QuantumCardProps {
6 className?: string
7 children: React.ReactNode
8 gradient?: boolean
9 hoverable?: boolean
10}
11
12export function QuantumCard({
13 className,
14 children,
15 gradient = false,
16 hoverable = false,
17}: QuantumCardProps) {
18 return (
19 <Card
20 className={cn(
21 "overflow-hidden",
22 hoverable && "transition-transform hover:-translate-y-1 hover:shadow-lg",
23 gradient && "bg-gradient-to-br from-blue-50 to-indigo-50 dark:from-blue-950 dark:to-indigo-950",
24 className
25 )}
26 >
27 {children}
28 </Card>
29 )
30}
31
32// Export remaining components
33export { CardContent, CardFooter, CardHeader }Personnaliser les fonctionnalités
Tu peux aussi étendre les fonctionnalités des composants :
1// components/quantum/quantum-button.tsx
2import { forwardRef } from "react"
3import { Button, ButtonProps } from "@/components/ui/button"
4import { Loader2 } from "lucide-react"
5
6interface QuantumButtonProps extends ButtonProps {
7 isLoading?: boolean
8 loadingText?: string
9 icon?: React.ReactNode
10 iconPosition?: "left" | "right"
11}
12
13export const QuantumButton = forwardRef<HTMLButtonElement, QuantumButtonProps>(
14 ({
15 children,
16 isLoading,
17 loadingText,
18 icon,
19 iconPosition = "left",
20 ...props
21 }, ref) => {
22 return (
23 <Button
24 ref={ref}
25 disabled={isLoading || props.disabled}
26 {...props}
27 >
28 {isLoading && (
29 <Loader2 className="mr-2 h-4 w-4 animate-spin" />
30 )}
31 {!isLoading && icon && iconPosition === "left" && (
32 <span className="mr-2">{icon}</span>
33 )}
34 {isLoading && loadingText ? loadingText : children}
35 {!isLoading && icon && iconPosition === "right" && (
36 <span className="ml-2">{icon}</span>
37 )}
38 </Button>
39 )
40 }
41)
42QuantumButton.displayName = "QuantumButton"Créer des thèmes et des design systems
Avec Shadcn/UI, tu peux facilement créer et gérer tes propres design systems :
Définir ton propre thème
- D'abord, définis des variables CSS pour différents thèmes :
1/* app/globals.css */
2@tailwind base;
3@tailwind components;
4@tailwind utilities;
5
6@layer base {
7 /* Default theme (light) */
8 :root {
9 --background: 0 0% 100%;
10 --foreground: 222.2 84% 4.9%;
11
12 /* Couleurs pour la marque Quantum */
13 --quantum-blue: 210 100% 50%;
14 --quantum-purple: 270 100% 60%;
15 --quantum-teal: 180 100% 40%;
16
17 /* Other variables */
18 /* ... */
19 }
20
21 /* Ciemny motyw */
22 .dark {
23 --background: 222.2 84% 4.9%;
24 --foreground: 210 40% 98%;
25
26 /* ... */
27 }
28
29 /* Motyw neonowy */
30 .theme-neon {
31 --primary: 120 100% 50%;
32 --secondary: 300 100% 50%;
33 --accent: 60 100% 50%;
34
35 /* ... */
36 }
37
38 /* Motyw vintage */
39 .theme-vintage {
40 --primary: 30 80% 60%;
41 --secondary: 20 60% 40%;
42 --accent: 40 70% 50%;
43
44 /* ... */
45 }
46}- Ensuite, crée un contexte pour la bascule de thème :
1// components/theme-provider.tsx
2"use client"
3
4import { createContext, useContext, useEffect, useState } from "react"
5
6type Theme = "light" | "dark" | "neon" | "vintage" | "system"
7
8type ThemeProviderProps = {
9 children: React.ReactNode
10 defaultTheme?: Theme
11 storageKey?: string
12}
13
14type ThemeProviderState = {
15 theme: Theme
16 setTheme: (theme: Theme) => void
17}
18
19const initialState: ThemeProviderState = {
20 theme: "system",
21 setTheme: () => null,
22}
23
24const ThemeProviderContext = createContext<ThemeProviderState>(initialState)
25
26export function ThemeProvider({
27 children,
28 defaultTheme = "system",
29 storageKey = "quantum-ui-theme",
30 ...props
31}: ThemeProviderProps) {
32 const [theme, setTheme] = useState<Theme>(
33 () => (localStorage.getItem(storageKey) as Theme) || defaultTheme
34 )
35
36 useEffect(() => {
37 const root = window.document.documentElement
38
39 // Remove previous theme classes
40 root.classList.remove("light", "dark", "theme-neon", "theme-vintage")
41
42 if (theme === "system") {
43 const systemTheme = window.matchMedia("(prefers-color-scheme: dark)")
44 .matches
45 ? "dark"
46 : "light"
47 root.classList.add(systemTheme)
48 return
49 }
50
51 if (theme === "neon") {
52 root.classList.add("dark", "theme-neon")
53 return
54 }
55
56 if (theme === "vintage") {
57 root.classList.add("light", "theme-vintage")
58 return
59 }
60
61 root.classList.add(theme)
62 }, [theme])
63
64 const value = {
65 theme,
66 setTheme: (theme: Theme) => {
67 localStorage.setItem(storageKey, theme)
68 setTheme(theme)
69 },
70 }
71
72 return (
73 <ThemeProviderContext.Provider {...props} value={value}>
74 {children}
75 </ThemeProviderContext.Provider>
76 )
77}
78
79export const useTheme = () => {
80 const context = useContext(ThemeProviderContext)
81
82 if (context === undefined)
83 throw new Error("useTheme must be used within a ThemeProvider")
84
85 return context
86}- Utilise le provider de thème dans ton layout :
1// app/layout.tsx
2import { ThemeProvider } from "@/components/theme-provider"
3import "./globals.css"
4
5export default function RootLayout({
6 children,
7}: {
8 children: React.ReactNode
9}) {
10 return (
11 <html lang="pl" suppressHydrationWarning>
12 <body>
13 <ThemeProvider defaultTheme="system" storageKey="quantum-theme">
14 {children}
15 </ThemeProvider>
16 </body>
17 </html>
18 )
19}Composant de bascule de thème
1// components/theme-switcher.tsx
2"use client"
3
4import { Moon, Sun, Sparkles, Clock } from "lucide-react"
5import { useTheme } from "@/components/theme-provider"
6import { Button } from "@/components/ui/button"
7import {
8 DropdownMenu,
9 DropdownMenuContent,
10 DropdownMenuItem,
11 DropdownMenuTrigger,
12} from "@/components/ui/dropdown-menu"
13
14export function ThemeSwitcher() {
15 const { theme, setTheme } = useTheme()
16
17 return (
18 <DropdownMenu>
19 <DropdownMenuTrigger asChild>
20 <Button variant="outline" size="icon">
21 <Sun className="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
22 <Moon className="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
23 <span className="sr-only">Toggle theme</span>
24 </Button>
25 </DropdownMenuTrigger>
26 <DropdownMenuContent align="end">
27 <DropdownMenuItem onClick={() => setTheme("light")}>
28 <Sun className="mr-2 h-4 w-4" />
29 <span>Jasny</span>
30 </DropdownMenuItem>
31 <DropdownMenuItem onClick={() => setTheme("dark")}>
32 <Moon className="mr-2 h-4 w-4" />
33 <span>Ciemny</span>
34 </DropdownMenuItem>
35 <DropdownMenuItem onClick={() => setTheme("neon")}>
36 <Sparkles className="mr-2 h-4 w-4" />
37 <span>Neon</span>
38 </DropdownMenuItem>
39 <DropdownMenuItem onClick={() => setTheme("vintage")}>
40 <Clock className="mr-2 h-4 w-4" />
41 <span>Vintage</span>
42 </DropdownMenuItem>
43 <DropdownMenuItem onClick={() => setTheme("system")}>
44 <span>Systemowy</span>
45 </DropdownMenuItem>
46 </DropdownMenuContent>
47 </DropdownMenu>
48 )
49}Bonnes pratiques de travail avec Shadcn/UI
1. Organisation des composants
Maintiens une structure de répertoires claire :
1components/
2├── ui/ # Podstawowe komponenty Shadcn/UI
3│ ├── button.tsx
4│ ├── card.tsx
5│ └──
6├── quantum/ # Your own business components
7│ ├── quantum-button.tsx
8│ ├── quantum-card.tsx
9│ └──
10└── blocks/ # Complex interface blocks
11 ├── dashboard-stats.tsx
12 ├── user-profile-form.tsx
13 └──2. Nommage cohérent
Maintiens des conventions de nommage cohérentes pour tes propres composants :
Button- le composant de base de Shadcn/UIQuantumButton- une extension personnalisée d'un composant de baseUserProfileForm- un composant métier complexe
3. Utiliser les générateurs de composants
Utilise le CLI de Shadcn/UI pour générer de nouveaux composants :
1npx shadcn@latest add alert4. Optimiser la réutilisabilité
Pour les combinaisons fréquemment utilisées, crée tes propres composants de plus haut niveau :
1// components/quantum/quantum-section.tsx
2import { cn } from "@/lib/utils"
3
4interface QuantumSectionProps {
5 title: string
6 description?: string
7 children: React.ReactNode
8 className?: string
9}
10
11export function QuantumSection({
12 title,
13 description,
14 children,
15 className,
16}: QuantumSectionProps) {
17 return (
18 <section className={cn("space-y-4", className)}>
19 <div className="space-y-1">
20 <h2 className="text-2xl font-bold">{title}</h2>
21 {description && (
22 <p className="text-muted-foreground">{description}</p>
23 )}
24 </div>
25 <div>{children}</div>
26 </section>
27 )
28}5. Gestion des thèmes
Utilise des variables CSS pour gérer les thèmes :
1// lib/themes.ts
2export const themes = {
3 default: {
4 primary: "222.2 47.4% 11.2%",
5 // ...inne kolory
6 },
7 quantum: {
8 primary: "240 100% 50%",
9 // ...inne kolory
10 },
11 // ...inne motywy
12}
13
14export function setTheme(theme: keyof typeof themes) {
15 const root = document.documentElement
16 Object.entries(themes[theme]).forEach(([key, value]) => {
17 root.style.setProperty(`--${key}`, value)
18 })
19}Intégration avec d'autres outils
1. Intégration avec les bibliothèques de formulaires
Shadcn/UI s'intègre bien avec des bibliothèques comme React Hook Form et Zod :
1npx shadcn@latest add form1// components/signup-form.tsx
2"use client"
3
4import { zodResolver } from "@hookform/resolvers/zod"
5import { useForm } from "react-hook-form"
6import * as z from "zod"
7
8import { Button } from "@/components/ui/button"
9import {
10 Form,
11 FormControl,
12 FormDescription,
13 FormField,
14 FormItem,
15 FormLabel,
16 FormMessage,
17} from "@/components/ui/form"
18import { Input } from "@/components/ui/input"
19
20const formSchema = z.object({
21 username: z.string().min(2).max(50),
22 email: z.string().email(),
23 password: z.string().min(8),
24})
25
26export function SignupForm() {
27 const form = useForm<z.infer<typeof formSchema>>({
28 resolver: zodResolver(formSchema),
29 defaultValues: {
30 username: "",
31 email: "",
32 password: "",
33 },
34 })
35
36 function onSubmit(values: z.infer<typeof formSchema>) {
37 console.log(values)
38 }
39
40 return (
41 <Form {...form}>
42 <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
43 <FormField
44 control={form.control}
45 name="username"
46 render={({ field }) => (
47 <FormItem>
48 <FormLabel>Username</FormLabel>
49 <FormControl>
50 <Input placeholder="Enter your username" {...field} />
51 </FormControl>
52 <FormDescription>
53 The name that will be displayed on your profile.
54 </FormDescription>
55 <FormMessage />
56 </FormItem>
57 )}
58 />
59 {/* Other form fields */}
60 <Button type="submit">Sign Up</Button>
61 </form>
62 </Form>
63 )
64}2. Intégration avec les bibliothèques d'animation
Shadcn/UI peut facilement être intégré avec des bibliothèques d'animation :
1// components/animated-card.tsx
2"use client"
3
4import { motion } from "framer-motion"
5import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
6
7interface AnimatedCardProps {
8 title: string
9 children: React.ReactNode
10}
11
12export function AnimatedCard({ title, children }: AnimatedCardProps) {
13 return (
14 <motion.div
15 initial={{ opacity: 0, y: 20 }}
16 animate={{ opacity: 1, y: 0 }}
17 transition={{ duration: 0.5 }}
18 >
19 <Card>
20 <CardHeader>
21 <CardTitle>{title}</CardTitle>
22 </CardHeader>
23 <CardContent>{children}</CardContent>
24 </Card>
25 </motion.div>
26 )
27}3. Intégration avec la gestion de l'état
Shadcn/UI fonctionne bien avec les bibliothèques de gestion de l'état :
1// store/theme-store.ts
2import { create } from 'zustand'
3
4type Theme = 'light' | 'dark' | 'system'
5
6interface ThemeState {
7 theme: Theme
8 setTheme: (theme: Theme) => void
9}
10
11export const useThemeStore = create<ThemeState>((set) => ({
12 theme: 'system',
13 setTheme: (theme) => set({ theme }),
14}))
15
16// components/theme-toggle.tsx
17"use client"
18
19import { Moon, Sun } from "lucide-react"
20import { useThemeStore } from "@/store/theme-store"
21import { Button } from "@/components/ui/button"
22import { useEffect } from "react"
23
24export function ThemeToggle() {
25 const { theme, setTheme } = useThemeStore()
26
27 useEffect(() => {
28 const root = document.documentElement
29
30 if (theme === "system") {
31 const systemTheme = window.matchMedia("(prefers-color-scheme: dark)")
32 .matches ? "dark" : "light"
33 root.classList.remove("light", "dark")
34 root.classList.add(systemTheme)
35 } else {
36 root.classList.remove("light", "dark")
37 root.classList.add(theme)
38 }
39 }, [theme])
40
41 return (
42 <Button
43 variant="outline"
44 size="icon"
45 onClick={() => setTheme(theme === "dark" ? "light" : "dark")}
46 >
47 <Sun className="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
48 <Moon className="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
49 <span className="sr-only">Toggle theme</span>
50 </Button>
51 )
52}Résumé
Shadcn/UI est un outil puissant pour créer des interfaces utilisateur belles, accessibles et cohérentes dans Next.js 16. Au lieu d'être une boîte noire difficile à personnaliser, Shadcn/UI fournit des composants de haute qualité, modifiables, qui font partie de ton code.
Les avantages les plus importants de Shadcn/UI :
- Le contrôle - les composants font partie de ton code, tu as donc un contrôle total sur eux
- La modularité - tu télécharges uniquement les composants dont tu as besoin
- Le style - une personnalisation facile de l'apparence avec Tailwind CSS
- L'accessibilité - des composants construits sur Radix UI qui garantissent un haut niveau d'accessibilité
- Le mode sombre intégré - une prise en charge des thèmes clair et sombre par défaut
- TypeScript - une prise en charge complète des types
Contrairement aux bibliothèques d'interface traditionnelles qui peuvent limiter ta créativité et rendre difficile la personnalisation selon les besoins uniques d'un projet, Shadcn/UI fournit des fondations solides sur lesquelles tu peux bâtir tes propres expériences utilisateur uniques - tout comme le système de construction modulaire de Metropolis Quantum permet aux architectes de créer des bâtiments uniques tout en maintenant l'esthétique cohérente de la ville.
Dans le prochain chapitre, nous étudierons le composant Image et l'optimisation des images dans Next.js 16, ce qui nous permettra d'améliorer encore la performance et l'expérience utilisateur de nos applications.
Code de cette leçon : app/page.tsx
1// Shadcn/UI Components Demo - Next.js 16
2'use client';
3
4import { useState } from 'react';
5
6// Simplified Shadcn/UI-style components for demonstration
7
8function Button({ variant = 'default', size = 'default', children, onClick, className = '' }) {
9 const baseStyles = 'inline-flex items-center justify-center rounded-md font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2';
10
11 const variants = {
12 default: 'bg-blue-600 text-white hover:bg-blue-700 focus:ring-blue-500',
13 destructive: 'bg-red-600 text-white hover:bg-red-700 focus:ring-red-500',
14 outline: 'border-2 border-gray-300 bg-transparent hover:bg-gray-100 focus:ring-gray-500',
15 ghost: 'hover:bg-gray-100 focus:ring-gray-500',
16 };
17
18 const sizes = {
19 default: 'h-10 px-4 py-2',
20 sm: 'h-8 px-3 text-sm',
21 lg: 'h-12 px-8 text-lg',
22 icon: 'h-10 w-10',
23 };
24
25 return (
26 <button
27 onClick={onClick}
28 className={`${baseStyles} ${variants[variant]} ${sizes[size]} ${className}`}
29 >
30 {children}
31 </button>
32 );
33}
34
35function Card({ title, description, children }) {
36 return (
37 <div className="rounded-lg border border-gray-200 bg-white shadow-sm">
38 <div className="p-6">
39 {title && <h3 className="text-lg font-semibold mb-1">{title}</h3>}
40 {description && <p className="text-sm text-gray-500 mb-4">{description}</p>}
41 {children}
42 </div>
43 </div>
44 );
45}
46
47function Badge({ variant = 'default', children }) {
48 const variants = {
49 default: 'bg-blue-100 text-blue-800',
50 success: 'bg-green-100 text-green-800',
51 warning: 'bg-yellow-100 text-yellow-800',
52 destructive: 'bg-red-100 text-red-800',
53 };
54
55 return (
56 <span className={`inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-semibold ${variants[variant]}`}>
57 {children}
58 </span>
59 );
60}
61
62export default function ShadcnDemo() {
63 const [count, setCount] = useState(0);
64
65 return (
66 <div className="min-h-screen bg-gray-50 p-8">
67 <div className="max-w-4xl mx-auto">
68 <h1 className="text-3xl font-bold mb-2">Shadcn/UI Components</h1>
69 <p className="text-gray-500 mb-8">Modular, customizable UI components for Next.js 16</p>
70
71 {/* Button Variants */}
72 <Card title="Button Variants" description="Different button styles for various actions">
73 <div className="flex flex-wrap gap-3">
74 <Button>Default</Button>
75 <Button variant="destructive">Destructive</Button>
76 <Button variant="outline">Outline</Button>
77 <Button variant="ghost">Ghost</Button>
78 </div>
79 <div className="flex flex-wrap gap-3 mt-4">
80 <Button size="sm">Small</Button>
81 <Button size="default">Default</Button>
82 <Button size="lg">Large</Button>
83 </div>
84 </Card>
85
86 {/* Badges */}
87 <Card title="Badges" description="Status indicators and labels" className="mt-6">
88 <div className="flex flex-wrap gap-2">
89 <Badge>Default</Badge>
90 <Badge variant="success">Success</Badge>
91 <Badge variant="warning">Warning</Badge>
92 <Badge variant="destructive">Error</Badge>
93 </div>
94 </Card>
95
96 {/* Interactive Counter */}
97 <Card title="Interactive Example" description="Components working together" className="mt-6">
98 <div className="flex items-center gap-4">
99 <Button variant="outline" onClick={() => setCount(c => c - 1)}>-</Button>
100 <span className="text-2xl font-bold w-12 text-center">{count}</span>
101 <Button variant="outline" onClick={() => setCount(c => c + 1)}>+</Button>
102 <Button variant="destructive" size="sm" onClick={() => setCount(0)}>
103 Reset
104 </Button>
105 </div>
106 </Card>
107 </div>
108 </div>
109 );
110}Tu as repéré une erreur dans cette leçon ?
Teste tes connaissances
Réponds aux questions de cette leçon. Choisis une réponse pour voir tout de suite si elle est correcte.
1. Le principal avantage des CSS Modules est :
2. Tailwind CSS est :
Tâches pratiques dans le jeu
- Éditeur de code
Révision (sujet du module 1) : Implémente un tableau de bord avec une mise en page CSS responsive
- Tri vertical
Classe les sélecteurs CSS de la spécificité la plus faible à la plus forte :
- Clique dans l'ordre
Reconstitue la syntaxe correcte d'une déclaration CSS Grid
- Éditeur de code
Crée un composant Button avec son propre fichier CSS Module contenant les styles d'un bouton primary et d'un bouton secondary