Cours Next.js · Module 3: Styles et optimisation visuelle

Shadcn/UI - Composants pour Next.js 16

23 min de lecture
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 :

  1. Aucune dépendance à une bibliothèque externe - les composants font partie de ton code
  2. Un contrôle total sur les styles - une modification facile de l'apparence des composants
  3. Construit sur Radix UI - des primitives solides et accessibles pour les composants interactifs
  4. Stylisé avec Tailwind CSS - un design system cohérent et facile à personnaliser
  5. Mode sombre par défaut - une prise en charge intégrée des thèmes clair et sombre
  6. Un typage TypeScript strict - une prise en charge complète des types
  7. La modularité - n'installe que les composants dont tu as besoin
  8. 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 init

Aprè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 :

  1. Le choix du style - généralement « Default » est un bon choix
  2. Le choix du chemin des composants - typiquement « src/components/ui »
  3. Le choix du jeu de couleurs - tu peux choisir par défaut ou personnalisé
  4. Veux-tu activer le mode sombre ? - généralement « oui »
  5. Le choix du préfixe des composants - typiquement aucun préfixe
  6. Le chemin du fichier globals.css - typiquement « app/globals.css »
  7. 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 button

Cette 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

  1. Button - des boutons avec différentes variantes
  2. Input - des champs de saisie
  3. Checkbox - des cases à cocher
  4. RadioGroup - des groupes d'options radio
  5. Select - des listes déroulantes
  6. Textarea - des champs de texte multilignes
  7. Label - des étiquettes pour les contrôles de formulaire

Composants de mise en page

  1. Card - des cards pour regrouper du contenu
  2. AspectRatio - le maintien des proportions des éléments
  3. ScrollArea - des zones de défilement
  4. Separator - des lignes de séparation
  5. Sheet - des panneaux latéraux coulissants

Composants de navigation

  1. Tabs - des onglets
  2. Navigation Menu - un menu de navigation
  3. Breadcrumb - un fil d'Ariane de navigation
  4. Pagination - la pagination

Composants de dialogue

  1. Dialog - des fenêtres de dialogue
  2. Alert Dialog - des alertes nécessitant une confirmation
  3. Drawer - des panneaux coulissants
  4. Popover - des bulles contextuelles
  5. Toast - des notifications

Composants de données

  1. Table - des tableaux
  2. Accordion - des sections dépliables
  3. Calendar - des calendriers
  4. Carousel - des carrousels

Composants de formulaire

  1. Form - une solution de formulaire complète
  2. Combobox - des champs de sélection avancés avec recherche
  3. Slider - des curseurs
  4. 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 :

  1. 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}
  1. 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}
  1. É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

  1. 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}
  1. 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}
  1. 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/UI
  • QuantumButton - une extension personnalisée d'un composant de base
  • UserProfileForm - 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 alert

4. 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 form
1// 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 :

  1. Le contrôle - les composants font partie de ton code, tu as donc un contrôle total sur eux
  2. La modularité - tu télécharges uniquement les composants dont tu as besoin
  3. Le style - une personnalisation facile de l'apparence avec Tailwind CSS
  4. L'accessibilité - des composants construits sur Radix UI qui garantissent un haut niveau d'accessibilité
  5. Le mode sombre intégré - une prise en charge des thèmes clair et sombre par défaut
  6. 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. 1. Le principal avantage des CSS Modules est :

  2. 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

Articles utiles