Usamos cookies para mejorar tu experiencia en el sitio
CodeWorlds

Shadcn/UI - Komponenty dla Next.js 15

W Metropolis Quantum, zaawansowane laboratoria badawcze opracowały modułowe, prefabrykowane elementy budowlane, które można łatwo łączyć i dostosowywać według potrzeb. Dzięki temu architekci mogą szybko tworzyć budynki o spójnej estetyce, jednocześnie zachowując elastyczność w projektowaniu unikalnych przestrzeni. Podobną rolę w świecie programowania frontendowego pełni biblioteka Shadcn/UI - kolekcja starannie zaprojektowanych i dostępnych komponentów interfejsu użytkownika, które można łatwo integrować i dostosowywać do potrzeb projektu.

Czym jest Shadcn/UI?

Shadcn/UI to zbiór komponentów UI zbudowanych z myślą o Radix UI, Tailwind CSS i React. W przeciwieństwie do tradycyjnych bibliotek komponentów, Shadcn/UI nie jest instalowane jako zależność npm. Zamiast tego, komponenty są kopiowane bezpośrednio do twojego projektu, co daje ci pełną kontrolę nad kodem i umożliwia łatwe modyfikacje według potrzeb.

Główne cechy Shadcn/UI:

  1. Brak zależności od zewnętrznej biblioteki - komponenty są częścią twojego kodu
  2. Pełna kontrola nad stylami - łatwa modyfikacja wyglądu komponentów
  3. Oparte na Radix UI - solidne, dostępne primitives dla interaktywnych komponentów
  4. Stylizowane z Tailwind CSS - spójny i łatwy do dostosowania design system
  5. Ciemny tryb w standardzie - wbudowana obsługa jasnego i ciemnego motywu
  6. Ścisła typizacja z TypeScript - pełne wsparcie dla typów
  7. Modularność - instaluj tylko te komponenty, których potrzebujesz
  8. Responsywność - komponenty dostosowują się do różnych rozmiarów ekranów

Instalacja i konfiguracja Shadcn/UI w Next.js 15

Aby rozpocząć pracę z Shadcn/UI w projekcie Next.js 15, musisz wykonać kilka kroków:

Krok 1: Instalacja CLI Shadcn/UI

1npx shadcn-ui@latest init

Po uruchomieniu tego polecenia, CLI poprosi cię o odpowiedź na kilka pytań:

  1. Wybór stylów - zazwyczaj "Default" jest dobrym wyborem
  2. Wybór ścieżki do komponentów - standardowo "src/components/ui"
  3. Wybór schematu kolorów - możesz wybrać domyślny lub własny
  4. Czy chcesz włączyć ciemny motyw? - zazwyczaj "yes"
  5. Wybór prefiksu dla komponentów - standardowo bez prefiksu
  6. Określenie ścieżki do pliku globals.css - standardowo "app/globals.css"
  7. Czy umieścić zmienne CSS w pliku globals.css? - zazwyczaj "yes"

Krok 2: Sprawdzenie konfiguracji Tailwind CSS

CLI automatycznie zaktualizuje plik konfiguracyjny Tailwind CSS. Warto sprawdzić zawartość pliku

tailwind.config.js
:

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}

Krok 3: Sprawdzenie zmiennych CSS

CLI doda również zmienne CSS do pliku

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}

Krok 4: Dodanie komponentów Shadcn/UI

Teraz możesz instalować wybrane komponenty za pomocą CLI:

1npx shadcn-ui@latest add button

To polecenie doda komponent Button do katalogu

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 }

CLI automatycznie doda również plik

lib/utils.ts
, który zawiera pomocnicze funkcje:

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}

Główne komponenty Shadcn/UI

Shadcn/UI oferuje wiele komponentów, od prostych przycisków po zaawansowane komponenty interakcji. Oto kilka najważniejszych:

Podstawowe komponenty

  1. Button - przyciski z różnymi wariantami
  2. Input - pola wprowadzania
  3. Checkbox - pola wyboru
  4. RadioGroup - grupy opcji radio
  5. Select - rozwijane listy wyboru
  6. Textarea - pola tekstowe wieloliniowe
  7. Label - etykiety dla kontrolek formularzy

Komponenty układu

  1. Card - karty do grupowania zawartości
  2. AspectRatio - zachowanie proporcji elementów
  3. ScrollArea - obszary przewijania
  4. Separator - linie oddzielające
  5. Sheet - wysuwane panele boczne

Komponenty nawigacji

  1. Tabs - zakładki
  2. Navigation Menu - menu nawigacyjne
  3. Breadcrumb - ścieżki nawigacyjne
  4. Pagination - stronicowanie

Komponenty dialogowe

  1. Dialog - okna dialogowe
  2. Alert Dialog - alerty wymagające potwierdzenia
  3. Drawer - wysuwane panele
  4. Popover - wyskakujące dymki
  5. Toast - powiadomienia

Komponenty danych

  1. Table - tabele
  2. Accordion - rozwijane sekcje
  3. Calendar - kalendarze
  4. Carousel - karuzele

Komponenty formularzy

  1. Form - kompleksowe rozwiązanie do formularzy
  2. Combobox - rozbudowane pola wyboru z wyszukiwaniem
  3. Slider - suwaki
  4. Switch - przełączniki

Przykłady użycia Shadcn/UI w projekcie Next.js 15

Przykład 1: Formularz logowania

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: "Wprowadź poprawny adres email.",
25  }),
26  password: z.string().min(8, {
27    message: "Hasło musi zawierać co najmniej 8 znaków.",
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      // W rzeczywistej aplikacji wywołalibyśmy tutaj API logowania
48      console.log(values)
49      
50      // Symulacja opóźnienia
51      await new Promise(resolve => setTimeout(resolve, 1000))
52      
53      toast({
54        title: "Zalogowano pomyślnie",
55        description: "Przekierowujemy Cię do panelu głównego",
56      })
57      
58      router.push("/dashboard")
59    } catch (error) {
60      toast({
61        variant: "destructive",
62        title: "Wystąpił błąd",
63        description: "Nie można zalogować. Sprawdź dane logowania i spróbuj ponownie.",
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          Wprowadź swoje dane, aby zalogować się do panelu Metropolis Quantum
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>Hasło</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 ? "Logowanie..." : "Zaloguj się"}
108          </Button>
109        </form>
110      </Form>
111      <div className="text-center text-sm">
112        <a href="/forgot-password" className="text-primary hover:underline">
113          Nie pamiętasz hasła?
114        </a>
115      </div>
116    </div>
117  )
118}

Przykład 2: Panel z zakładkami

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">Przegląd</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">Całkowity Przychód</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 zł</div>
41                <p className="text-xs text-muted-foreground">
42                  +20.1% od ostatniego miesiąca
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% od ostatniego miesiąca
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">Sprzedaże</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% od ostatniego miesiąca
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">Aktywni Użytkownicy</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 od ostatniego miesiąca
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>Przegląd</CardTitle>
123              </CardHeader>
124              <CardContent className="pl-2">
125                {/* Tutaj mógłby być wykres */}
126                <div className="h-[200px] bg-muted rounded-md flex items-center justify-center">
127                  Wykres Przychodów
128                </div>
129              </CardContent>
130            </Card>
131            <Card className="col-span-3">
132              <CardHeader>
133                <CardTitle>Ostatnie Transakcje</CardTitle>
134                <CardDescription>
135                  Wykonano 24 transakcje w tym miesiącu.
136                </CardDescription>
137              </CardHeader>
138              <CardContent>
139                <div className="space-y-8">
140                  {/* Przykładowa transakcja */}
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 zł</div>
161                  </div>
162                  {/* Więcej transakcji */}
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                Szczegółowa analiza ruchu i konwersji.
175              </CardDescription>
176            </CardHeader>
177            <CardContent>
178              <div className="h-[300px] bg-muted rounded-md flex items-center justify-center">
179                Dane Analityczne
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                Przeglądaj i generuj raporty z działalności.
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">Raport Miesięczny (Kwiecień 2024)</p>
198                    <p className="text-xs text-muted-foreground">Utworzony 01.05.2024</p>
199                  </div>
200                  <Button variant="outline" size="sm">Pobierz</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">Utworzony 05.04.2024</p>
206                  </div>
207                  <Button variant="outline" size="sm">Pobierz</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                Zarządzaj ustawieniami swojego konta.
223              </CardDescription>
224            </CardHeader>
225            <CardContent className="space-y-4">
226              {/* Przykładowe ustawienia */}
227              <div className="space-y-1">
228                <p className="text-sm font-medium">Powiadomienia Email</p>
229                <p className="text-xs text-muted-foreground">Otrzymuj powiadomienia o ważnych aktualizacjach.</p>
230              </div>
231              {/* Więcej ustawień */}
232            </CardContent>
233            <CardFooter>
234              <Button>Zapisz Zmiany</Button>
235            </CardFooter>
236          </Card>
237        </TabsContent>
238      </Tabs>
239    </div>
240  )
241}

Przykład 3: Dialog z formularzem

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: "Projekt utworzony",
51      description: `Projekt "${formData.name}" został pomyślnie utworzony.`,
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>Utwórz nowy projekt</DialogTitle>
74            <DialogDescription>
75              Wprowadź szczegóły projektu. Kliknij zapisz, gdy skończysz.
76            </DialogDescription>
77          </DialogHeader>
78          <div className="grid gap-4 py-4">
79            <div className="grid gap-2">
80              <Label htmlFor="name">Nazwa projektu</Label>
81              <Input
82                id="name"
83                value={formData.name}
84                onChange={(e) => handleChange("name", e.target.value)}
85                placeholder="Nazwa projektu"
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="Opisz swój projekt"
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="Wybierz kategorię" />
109                  </SelectTrigger>
110                  <SelectContent>
111                    <SelectItem value="development">Rozwój</SelectItem>
112                    <SelectItem value="research">Badania</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="Wybierz priorytet" />
128                  </SelectTrigger>
129                  <SelectContent>
130                    <SelectItem value="low">Niski</SelectItem>
131                    <SelectItem value="medium">Średni</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              Anuluj
142            </Button>
143            <Button type="submit" disabled={isLoading}>
144              {isLoading ? "Zapisywanie..." : "Zapisz projekt"}
145            </Button>
146          </DialogFooter>
147        </form>
148      </DialogContent>
149    </Dialog>
150  )
151}

Dostosowywanie komponentów Shadcn/UI

Jedną z największych zalet Shadcn/UI jest łatwość dostosowywania komponentów. Ponieważ komponenty są częścią twojego kodu, możesz je modyfikować według potrzeb.

Dostosowywanie stylów

Możesz zmodyfikować wygląd komponentów na różne sposoby:

  1. Zmiana zmiennych CSS - aktualizuj wartości zmiennych w
    globals.css
1:root {
2  --primary: 240 5.9% 10%; /* Zmiana koloru primary */
3  --radius: 0.75rem; /* Zmiana zaokrąglenia narożników */
4}
  1. Modyfikacja definicji komponentu - edytuj bezpośrednio plik komponentu
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        // Dodajemy nowy wariant
14        gradient: "bg-gradient-to-r from-blue-500 to-purple-500 text-white hover:opacity-90",
15      },
16      // Pozostałe definicje
17    },
18  }
19)
20
21// Aktualizacja interfejsu, aby zawierał nowy wariant
22export interface ButtonProps
23  extends React.ButtonHTMLAttributes<HTMLButtonElement>,
24    VariantProps<typeof buttonVariants> {
25  asChild?: boolean
26}
  1. Rozszerzanie komponentów - twórz własne komponenty bazujące na 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// Eksportujemy pozostałe komponenty
33export { CardContent, CardFooter, CardHeader }

Dostosowywanie funkcjonalności

Możesz również rozszerzyć funkcjonalność komponentów:

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"

Tworzenie motywów i systemów projektowych

Z pomocą Shadcn/UI możesz łatwo tworzyć i zarządzać własnymi systemami projektowymi:

Zdefiniowanie własnego motywu

  1. Najpierw zdefiniuj zmienne CSS dla różnych motywów:
1/* app/globals.css */
2@tailwind base;
3@tailwind components;
4@tailwind utilities;
5 
6@layer base {
7  /* Domyślny motyw (jasny) */
8  :root {
9    --background: 0 0% 100%;
10    --foreground: 222.2 84% 4.9%;
11    
12    /* Kolory dla marki Quantum */
13    --quantum-blue: 210 100% 50%;
14    --quantum-purple: 270 100% 60%;
15    --quantum-teal: 180 100% 40%;
16    
17    /* Pozostałe zmienne */
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. Następnie utwórz kontekst dla przełączania motywów:
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    // Usuń poprzednie klasy motywów
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. Użyj dostawcy motywu w swoim layoucie:
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}

Komponent przełącznika motywów

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">Przełącz motyw</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}

Najlepsze praktyki pracy z Shadcn/UI

1. Organizacja komponentów

Utrzymuj przejrzystą strukturę katalogów:

1components/
2├── ui/              # Podstawowe komponenty Shadcn/UI
3│   ├── button.tsx
4│   ├── card.tsx
5│   └──
6├── quantum/         # Twoje własne komponenty biznesowe
7│   ├── quantum-button.tsx
8│   ├── quantum-card.tsx
9│   └──
10└── blocks/          # Złożone bloki interfejsu
11    ├── dashboard-stats.tsx
12    ├── user-profile-form.tsx
13    └──

2. Spójne nazewnictwo

Utrzymuj spójne konwencje nazewnictwa dla własnych komponentów:

  • Button
    - podstawowy komponent Shadcn/UI
  • QuantumButton
    - własne rozszerzenie podstawowego komponentu
  • UserProfileForm
    - złożony komponent biznesowy

3. Wykorzystanie generatorów komponentów

Używaj CLI Shadcn/UI do generowania nowych komponentów:

1npx shadcn-ui@latest add alert

4. Optymalizacja wielokrotnego używania

Dla często używanych kombinacji, twórz własne komponenty wyższego poziomu:

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. Zarządzanie motywami

Używaj zmiennych CSS, aby zarządzać motywami:

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}

Integracja z innymi narzędziami

1. Integracja z bibliotekami formularzy

Shadcn/UI dobrze integruje się z bibliotekami takimi jak React Hook Form i Zod:

1npx shadcn-ui@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>Nazwa użytkownika</FormLabel>
49              <FormControl>
50                <Input placeholder="Wpisz nazwę użytkownika" {...field} />
51              </FormControl>
52              <FormDescription>
53                Nazwa, która będzie wyświetlana w Twoim profilu.
54              </FormDescription>
55              <FormMessage />
56            </FormItem>
57          )}
58        />
59        {/* Pozostałe pola formularza */}
60        <Button type="submit">Zarejestruj się</Button>
61      </form>
62    </Form>
63  )
64}

2. Integracja z bibliotekami animacji

Shadcn/UI można łatwo zintegrować z bibliotekami animacji:

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. Integracja z zarządzaniem stanem

Shadcn/UI dobrze współpracuje z bibliotekami do zarządzania stanem:

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">Przełącz motyw</span>
50    </Button>
51  )
52}

Podsumowanie

Shadcn/UI to potężne narzędzie do tworzenia pięknych, dostępnych i spójnych interfejsów użytkownika w Next.js 15. Zamiast być czarną skrzynką, której trudno dostosować, Shadcn/UI dostarcza wysokiej jakości, modyfikowalne komponenty, które stają się częścią twojego kodu.

Najważniejsze zalety Shadcn/UI:

  1. Kontrola - komponenty są częścią twojego kodu, więc masz nad nimi pełną kontrolę
  2. Modularność - pobierasz tylko te komponenty, których potrzebujesz
  3. Stylizacja - łatwe dostosowanie wyglądu za pomocą Tailwind CSS
  4. Dostępność - komponenty zbudowane na Radix UI, które zapewniają wysoki poziom dostępności
  5. Wbudowany ciemny motyw - wsparcie dla jasnego i ciemnego motywu w standardzie
  6. TypeScript - pełne wsparcie dla typów

W przeciwieństwie do tradycyjnych bibliotek UI, które mogą ograniczać twoją kreatywność i utrudniać dostosowanie do unikalnych potrzeb projektu, Shadcn/UI zapewnia solidne fundamenty, na których możesz budować własne, niepowtarzalne doświadczenia użytkownika - podobnie jak modułowy system budowy w Metropolis Quantum pozwala architektom tworzyć unikalne budynki przy jednoczesnym zachowaniu spójnej estetyki miasta.

W następnym rozdziale przyjrzymy się komponentowi Image i optymalizacji obrazów w Next.js 15, co pozwoli nam dalej poprawić wydajność i user experience naszych aplikacji.

Ir a CodeWorlds