Kurs Next.js · Moduł 3: Stylowanie i optymalizacja wizualna
Shadcn/UI - Komponenty dla Next.js 16
W tej lekcji9
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:
- Brak zależności od zewnętrznej biblioteki - komponenty są częścią twojego kodu
- Pełna kontrola nad stylami - łatwa modyfikacja wyglądu komponentów
- Oparte na Radix UI - solidne, dostępne primitives dla interaktywnych komponentów
- Stylizowane z Tailwind CSS - spójny i łatwy do dostosowania design system
- Ciemny tryb w standardzie - wbudowana obsługa jasnego i ciemnego motywu
- Ścisła typizacja z TypeScript - pełne wsparcie dla typów
- Modularność - instaluj tylko te komponenty, których potrzebujesz
- Responsywność - komponenty dostosowują się do różnych rozmiarów ekranów
Instalacja i konfiguracja Shadcn/UI w Next.js 16
Aby rozpocząć pracę z Shadcn/UI w projekcie Next.js 16, musisz wykonać kilka kroków:
Krok 1: Instalacja CLI Shadcn/UI
npx shadcn@latest initPo uruchomieniu tego polecenia CLI wykryje projekt Next.js, zapisze ustawienia w pliku components.json i zada kilka pytań. Starsze wersje narzędzia, wydawane pod nazwą pakietu shadcn-ui, pytały m.in. o:
- Wybór stylów - zazwyczaj "Default" jest dobrym wyborem
- Wybór ścieżki do komponentów - standardowo "src/components/ui"
- Wybór schematu kolorów - możesz wybrać domyślny lub własny
- Czy chcesz włączyć ciemny motyw? - zazwyczaj "yes"
- Wybór prefiksu dla komponentów - standardowo bez prefiksu
- Określenie ścieżki do pliku globals.css - standardowo "app/globals.css"
- Czy umieścić zmienne CSS w pliku globals.css? - zazwyczaj "yes"
Krok 2: Sprawdzenie konfiguracji Tailwind CSS
W projekcie z Tailwind CSS 4, który create-next-app instaluje w Next.js 16, CLI zapisuje kolory i zaokrąglenia jako zmienne CSS w globals.css (w bloku @theme inline) i nie tworzy pliku tailwind.config.js. W projektach z Tailwind CSS 3 CLI aktualizuje właśnie ten plik - warto wtedy sprawdzić jego zawartość:
// tailwind.config.js
/** @type {import('tailwindcss').Config} */
module.exports = {
darkMode: ["class"],
content: [
'./pages/**/*.{js,ts,jsx,tsx,mdx}',
'./components/**/*.{js,ts,jsx,tsx,mdx}',
'./app/**/*.{js,ts,jsx,tsx,mdx}',
],
theme: {
container: {
center: true,
padding: "2rem",
screens: {
"2xl": "1400px",
},
},
extend: {
colors: {
border: "hsl(var(--border))",
input: "hsl(var(--input))",
ring: "hsl(var(--ring))",
background: "hsl(var(--background))",
foreground: "hsl(var(--foreground))",
primary: {
DEFAULT: "hsl(var(--primary))",
foreground: "hsl(var(--primary-foreground))",
},
secondary: {
DEFAULT: "hsl(var(--secondary))",
foreground: "hsl(var(--secondary-foreground))",
},
destructive: {
DEFAULT: "hsl(var(--destructive))",
foreground: "hsl(var(--destructive-foreground))",
},
muted: {
DEFAULT: "hsl(var(--muted))",
foreground: "hsl(var(--muted-foreground))",
},
accent: {
DEFAULT: "hsl(var(--accent))",
foreground: "hsl(var(--accent-foreground))",
},
popover: {
DEFAULT: "hsl(var(--popover))",
foreground: "hsl(var(--popover-foreground))",
},
card: {
DEFAULT: "hsl(var(--card))",
foreground: "hsl(var(--card-foreground))",
},
},
borderRadius: {
lg: "var(--radius)",
md: "calc(var(--radius) - 2px)",
sm: "calc(var(--radius) - 4px)",
},
keyframes: {
"accordion-down": {
from: { height: 0 },
to: { height: "var(--radix-accordion-content-height)" },
},
"accordion-up": {
from: { height: "var(--radix-accordion-content-height)" },
to: { height: 0 },
},
},
animation: {
"accordion-down": "accordion-down 0.2s ease-out",
"accordion-up": "accordion-up 0.2s ease-out",
},
},
},
plugins: [require("tailwindcss-animate")],
}Krok 3: Sprawdzenie zmiennych CSS
CLI doda również zmienne CSS do pliku globals.css:
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;
--card: 0 0% 100%;
--card-foreground: 222.2 84% 4.9%;
--popover: 0 0% 100%;
--popover-foreground: 222.2 84% 4.9%;
--primary: 222.2 47.4% 11.2%;
--primary-foreground: 210 40% 98%;
--secondary: 210 40% 96.1%;
--secondary-foreground: 222.2 47.4% 11.2%;
--muted: 210 40% 96.1%;
--muted-foreground: 215.4 16.3% 46.9%;
--accent: 210 40% 96.1%;
--accent-foreground: 222.2 47.4% 11.2%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 210 40% 98%;
--border: 214.3 31.8% 91.4%;
--input: 214.3 31.8% 91.4%;
--ring: 222.2 84% 4.9%;
--radius: 0.5rem;
}
.dark {
--background: 222.2 84% 4.9%;
--foreground: 210 40% 98%;
--card: 222.2 84% 4.9%;
--card-foreground: 210 40% 98%;
--popover: 222.2 84% 4.9%;
--popover-foreground: 210 40% 98%;
--primary: 210 40% 98%;
--primary-foreground: 222.2 47.4% 11.2%;
--secondary: 217.2 32.6% 17.5%;
--secondary-foreground: 210 40% 98%;
--muted: 217.2 32.6% 17.5%;
--muted-foreground: 215 20.2% 65.1%;
--accent: 217.2 32.6% 17.5%;
--accent-foreground: 210 40% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 210 40% 98%;
--border: 217.2 32.6% 17.5%;
--input: 217.2 32.6% 17.5%;
--ring: 212.7 26.8% 83.9%;
}
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
}Krok 4: Dodanie komponentów Shadcn/UI
Teraz możesz instalować wybrane komponenty za pomocą CLI:
npx shadcn@latest add buttonTo polecenie doda komponent Button do katalogu components/ui:
// components/ui/button.tsx
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const buttonVariants = cva(
"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",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive:
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
outline:
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-10 px-4 py-2",
sm: "h-9 rounded-md px-3",
lg: "h-11 rounded-md px-8",
icon: "h-10 w-10",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button"
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
)
}
)
Button.displayName = "Button"
export { Button, buttonVariants }CLI automatycznie doda również plik lib/utils.ts, który zawiera pomocnicze funkcje:
// lib/utils.ts
import { type ClassValue, clsx } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}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
- Button - przyciski z różnymi wariantami
- Input - pola wprowadzania
- Checkbox - pola wyboru
- RadioGroup - grupy opcji radio
- Select - rozwijane listy wyboru
- Textarea - pola tekstowe wieloliniowe
- Label - etykiety dla kontrolek formularzy
Komponenty układu
- Card - karty do grupowania zawartości
- AspectRatio - zachowanie proporcji elementów
- ScrollArea - obszary przewijania
- Separator - linie oddzielające
- Sheet - wysuwane panele boczne
Komponenty nawigacji
- Tabs - zakładki
- Navigation Menu - menu nawigacyjne
- Breadcrumb - ścieżki nawigacyjne
- Pagination - stronicowanie
Komponenty dialogowe
- Dialog - okna dialogowe
- Alert Dialog - alerty wymagające potwierdzenia
- Drawer - wysuwane panele
- Popover - wyskakujące dymki
- Toast - powiadomienia
Komponenty danych
- Table - tabele
- Accordion - rozwijane sekcje
- Calendar - kalendarze
- Carousel - karuzele
Komponenty formularzy
- Form - kompleksowe rozwiązanie do formularzy
- Combobox - rozbudowane pola wyboru z wyszukiwaniem
- Slider - suwaki
- Switch - przełączniki
Przykłady użycia Shadcn/UI w projekcie Next.js 16
Przykład 1: Formularz logowania
// app/login/page.tsx
"use client"
import { useState } from "react"
import { useRouter } from "next/navigation"
import { zodResolver } from "@hookform/resolvers/zod"
import { useForm } from "react-hook-form"
import * as z from "zod"
import { Button } from "@/components/ui/button"
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form"
import { Input } from "@/components/ui/input"
import { toast } from "@/components/ui/use-toast"
const formSchema = z.object({
email: z.string().email({
message: "Wprowadź poprawny adres email.",
}),
password: z.string().min(8, {
message: "Hasło musi zawierać co najmniej 8 znaków.",
}),
})
export default function LoginPage() {
const router = useRouter()
const [isLoading, setIsLoading] = useState(false)
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
email: "",
password: "",
},
})
async function onSubmit(values: z.infer<typeof formSchema>) {
setIsLoading(true)
try {
// W rzeczywistej aplikacji wywołalibyśmy tutaj API logowania
console.log(values)
// Symulacja opóźnienia
await new Promise(resolve => setTimeout(resolve, 1000))
toast({
title: "Zalogowano pomyślnie",
description: "Przekierowujemy Cię do panelu głównego",
})
router.push("/dashboard")
} catch (error) {
toast({
variant: "destructive",
title: "Wystąpił błąd",
description: "Nie można zalogować. Sprawdź dane logowania i spróbuj ponownie.",
})
} finally {
setIsLoading(false)
}
}
return (
<div className="mx-auto max-w-md space-y-6 p-6">
<div className="space-y-2 text-center">
<h1 className="text-3xl font-bold">Logowanie</h1>
<p className="text-gray-500 dark:text-gray-400">
Wprowadź swoje dane, aby zalogować się do panelu Metropolis Quantum
</p>
</div>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input type="email" placeholder="email@example.com" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="password"
render={({ field }) => (
<FormItem>
<FormLabel>Hasło</FormLabel>
<FormControl>
<Input type="password" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit" className="w-full" disabled={isLoading}>
{isLoading ? "Logowanie..." : "Zaloguj się"}
</Button>
</form>
</Form>
<div className="text-center text-sm">
<a href="/forgot-password" className="text-primary hover:underline">
Nie pamiętasz hasła?
</a>
</div>
</div>
)
}Przykład 2: Panel z zakładkami
// app/dashboard/page.tsx
"use client"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"
import { Button } from "@/components/ui/button"
export default function DashboardPage() {
return (
<div className="container mx-auto py-10">
<h1 className="text-3xl font-bold mb-6">Dashboard Metropolis Quantum</h1>
<Tabs defaultValue="overview" className="space-y-4">
<TabsList className="grid w-full grid-cols-4">
<TabsTrigger value="overview">Przegląd</TabsTrigger>
<TabsTrigger value="analytics">Analityka</TabsTrigger>
<TabsTrigger value="reports">Raporty</TabsTrigger>
<TabsTrigger value="settings">Ustawienia</TabsTrigger>
</TabsList>
<TabsContent value="overview" className="space-y-4">
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Całkowity Przychód</CardTitle>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
className="h-4 w-4 text-muted-foreground"
>
<path d="M12 2v20M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6" />
</svg>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">45,231.89 zł</div>
<p className="text-xs text-muted-foreground">
+20.1% od ostatniego miesiąca
</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Subskrypcje</CardTitle>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
className="h-4 w-4 text-muted-foreground"
>
<path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2" />
<circle cx="9" cy="7" r="4" />
<path d="M22 21v-2a4 4 0 0 0-3-3.87M16 3.13a4 4 0 0 1 0 7.75" />
</svg>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">+2350</div>
<p className="text-xs text-muted-foreground">
+180.1% od ostatniego miesiąca
</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Sprzedaże</CardTitle>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
className="h-4 w-4 text-muted-foreground"
>
<rect width="20" height="14" x="2" y="5" rx="2" />
<path d="M2 10h20" />
</svg>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">+12,234</div>
<p className="text-xs text-muted-foreground">
+19% od ostatniego miesiąca
</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Aktywni Użytkownicy</CardTitle>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
className="h-4 w-4 text-muted-foreground"
>
<path d="M22 12h-4l-3 9L9 3l-3 9H2" />
</svg>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">+573</div>
<p className="text-xs text-muted-foreground">
+201 od ostatniego miesiąca
</p>
</CardContent>
</Card>
</div>
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-7">
<Card className="col-span-4">
<CardHeader>
<CardTitle>Przegląd</CardTitle>
</CardHeader>
<CardContent className="pl-2">
{/* Tutaj mógłby być wykres */}
<div className="h-[200px] bg-muted rounded-md flex items-center justify-center">
Wykres Przychodów
</div>
</CardContent>
</Card>
<Card className="col-span-3">
<CardHeader>
<CardTitle>Ostatnie Transakcje</CardTitle>
<CardDescription>
Wykonano 24 transakcje w tym miesiącu.
</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-8">
{/* Przykładowa transakcja */}
<div className="flex items-center">
<div className="mr-4 rounded-full bg-primary/10 p-2">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
className="h-4 w-4 text-primary"
>
<path d="M12 2v20M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6" />
</svg>
</div>
<div className="space-y-1">
<p className="text-sm font-medium">Quantum Plan (Roczny)</p>
<p className="text-xs text-muted-foreground">14 kwietnia 2024</p>
</div>
<div className="ml-auto font-medium">+4,999.00 zł</div>
</div>
{/* Więcej transakcji */}
</div>
</CardContent>
</Card>
</div>
</TabsContent>
<TabsContent value="analytics" className="space-y-4">
<Card className="col-span-4">
<CardHeader>
<CardTitle>Analityka</CardTitle>
<CardDescription>
Szczegółowa analiza ruchu i konwersji.
</CardDescription>
</CardHeader>
<CardContent>
<div className="h-[300px] bg-muted rounded-md flex items-center justify-center">
Dane Analityczne
</div>
</CardContent>
</Card>
</TabsContent>
<TabsContent value="reports" className="space-y-4">
<Card>
<CardHeader>
<CardTitle>Raporty</CardTitle>
<CardDescription>
Przeglądaj i generuj raporty z działalności.
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium">Raport Miesięczny (Kwiecień 2024)</p>
<p className="text-xs text-muted-foreground">Utworzony 01.05.2024</p>
</div>
<Button variant="outline" size="sm">Pobierz</Button>
</div>
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium">Raport Kwartalny (Q1 2024)</p>
<p className="text-xs text-muted-foreground">Utworzony 05.04.2024</p>
</div>
<Button variant="outline" size="sm">Pobierz</Button>
</div>
</div>
</CardContent>
<CardFooter>
<Button>Generuj Nowy Raport</Button>
</CardFooter>
</Card>
</TabsContent>
<TabsContent value="settings" className="space-y-4">
<Card>
<CardHeader>
<CardTitle>Ustawienia</CardTitle>
<CardDescription>
Zarządzaj ustawieniami swojego konta.
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
{/* Przykładowe ustawienia */}
<div className="space-y-1">
<p className="text-sm font-medium">Powiadomienia Email</p>
<p className="text-xs text-muted-foreground">Otrzymuj powiadomienia o ważnych aktualizacjach.</p>
</div>
{/* Więcej ustawień */}
</CardContent>
<CardFooter>
<Button>Zapisz Zmiany</Button>
</CardFooter>
</Card>
</TabsContent>
</Tabs>
</div>
)
}Przykład 3: Dialog z formularzem
// components/quantum-project-dialog.tsx
"use client"
import { useState } from "react"
import { Button } from "@/components/ui/button"
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { Textarea } from "@/components/ui/textarea"
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select"
import { toast } from "@/components/ui/use-toast"
export function QuantumProjectDialog() {
const [open, setOpen] = useState(false)
const [isLoading, setIsLoading] = useState(false)
const [formData, setFormData] = useState({
name: "",
description: "",
category: "",
priority: "",
})
const handleChange = (field: string, value: string) => {
setFormData((prev) => ({ ...prev, [field]: value }))
}
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
setIsLoading(true)
// Symulacja zapisywania danych
await new Promise(resolve => setTimeout(resolve, 1000))
toast({
title: "Projekt utworzony",
description: `Projekt "${formData.name}" został pomyślnie utworzony.`,
})
setIsLoading(false)
setOpen(false)
// Reset formularza
setFormData({
name: "",
description: "",
category: "",
priority: "",
})
}
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<Button variant="default">Nowy Projekt</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-[425px]">
<form onSubmit={handleSubmit}>
<DialogHeader>
<DialogTitle>Utwórz nowy projekt</DialogTitle>
<DialogDescription>
Wprowadź szczegóły projektu. Kliknij zapisz, gdy skończysz.
</DialogDescription>
</DialogHeader>
<div className="grid gap-4 py-4">
<div className="grid gap-2">
<Label htmlFor="name">Nazwa projektu</Label>
<Input
id="name"
value={formData.name}
onChange={(e) => handleChange("name", e.target.value)}
placeholder="Nazwa projektu"
required
/>
</div>
<div className="grid gap-2">
<Label htmlFor="description">Opis</Label>
<Textarea
id="description"
value={formData.description}
onChange={(e) => handleChange("description", e.target.value)}
placeholder="Opisz swój projekt"
required
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="grid gap-2">
<Label htmlFor="category">Kategoria</Label>
<Select
value={formData.category}
onValueChange={(value) => handleChange("category", value)}
required
>
<SelectTrigger id="category">
<SelectValue placeholder="Wybierz kategorię" />
</SelectTrigger>
<SelectContent>
<SelectItem value="development">Rozwój</SelectItem>
<SelectItem value="research">Badania</SelectItem>
<SelectItem value="infrastructure">Infrastruktura</SelectItem>
<SelectItem value="marketing">Marketing</SelectItem>
<SelectItem value="other">Inne</SelectItem>
</SelectContent>
</Select>
</div>
<div className="grid gap-2">
<Label htmlFor="priority">Priorytet</Label>
<Select
value={formData.priority}
onValueChange={(value) => handleChange("priority", value)}
required
>
<SelectTrigger id="priority">
<SelectValue placeholder="Wybierz priorytet" />
</SelectTrigger>
<SelectContent>
<SelectItem value="low">Niski</SelectItem>
<SelectItem value="medium">Średni</SelectItem>
<SelectItem value="high">Wysoki</SelectItem>
<SelectItem value="critical">Krytyczny</SelectItem>
</SelectContent>
</Select>
</div>
</div>
</div>
<DialogFooter>
<Button type="button" variant="outline" onClick={() => setOpen(false)}>
Anuluj
</Button>
<Button type="submit" disabled={isLoading}>
{isLoading ? "Zapisywanie..." : "Zapisz projekt"}
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
)
}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:
- Zmiana zmiennych CSS - aktualizuj wartości zmiennych w
globals.css
:root {
--primary: 240 5.9% 10%; /* Zmiana koloru primary */
--radius: 0.75rem; /* Zmiana zaokrąglenia narożników */
}- Modyfikacja definicji komponentu - edytuj bezpośrednio plik komponentu
// components/ui/button.tsx
const buttonVariants = cva(
"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",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
outline: "border border-input hover:bg-accent hover:text-accent-foreground",
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "underline-offset-4 hover:underline text-primary",
// Dodajemy nowy wariant
gradient: "bg-gradient-to-r from-blue-500 to-purple-500 text-white hover:opacity-90",
},
// Pozostałe definicje
},
}
)
// Aktualizacja interfejsu, aby zawierał nowy wariant
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean
}- Rozszerzanie komponentów - twórz własne komponenty bazujące na Shadcn/UI
// components/quantum/quantum-card.tsx
import { Card, CardContent, CardFooter, CardHeader } from "@/components/ui/card"
import { cn } from "@/lib/utils"
interface QuantumCardProps {
className?: string
children: React.ReactNode
gradient?: boolean
hoverable?: boolean
}
export function QuantumCard({
className,
children,
gradient = false,
hoverable = false,
}: QuantumCardProps) {
return (
<Card
className={cn(
"overflow-hidden",
hoverable && "transition-transform hover:-translate-y-1 hover:shadow-lg",
gradient && "bg-gradient-to-br from-blue-50 to-indigo-50 dark:from-blue-950 dark:to-indigo-950",
className
)}
>
{children}
</Card>
)
}
// Eksportujemy pozostałe komponenty
export { CardContent, CardFooter, CardHeader }Dostosowywanie funkcjonalności
Możesz również rozszerzyć funkcjonalność komponentów:
// components/quantum/quantum-button.tsx
import { forwardRef } from "react"
import { Button, ButtonProps } from "@/components/ui/button"
import { Loader2 } from "lucide-react"
interface QuantumButtonProps extends ButtonProps {
isLoading?: boolean
loadingText?: string
icon?: React.ReactNode
iconPosition?: "left" | "right"
}
export const QuantumButton = forwardRef<HTMLButtonElement, QuantumButtonProps>(
({
children,
isLoading,
loadingText,
icon,
iconPosition = "left",
...props
}, ref) => {
return (
<Button
ref={ref}
disabled={isLoading || props.disabled}
{...props}
>
{isLoading && (
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
)}
{!isLoading && icon && iconPosition === "left" && (
<span className="mr-2">{icon}</span>
)}
{isLoading && loadingText ? loadingText : children}
{!isLoading && icon && iconPosition === "right" && (
<span className="ml-2">{icon}</span>
)}
</Button>
)
}
)
QuantumButton.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
- Najpierw zdefiniuj zmienne CSS dla różnych motywów:
/* app/globals.css */
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
/* Domyślny motyw (jasny) */
:root {
--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;
/* Kolory dla marki Quantum */
--quantum-blue: 210 100% 50%;
--quantum-purple: 270 100% 60%;
--quantum-teal: 180 100% 40%;
/* Pozostałe zmienne */
/* ... */
}
/* Ciemny motyw */
.dark {
--background: 222.2 84% 4.9%;
--foreground: 210 40% 98%;
/* ... */
}
/* Motyw neonowy */
.theme-neon {
--primary: 120 100% 50%;
--secondary: 300 100% 50%;
--accent: 60 100% 50%;
/* ... */
}
/* Motyw vintage */
.theme-vintage {
--primary: 30 80% 60%;
--secondary: 20 60% 40%;
--accent: 40 70% 50%;
/* ... */
}
}- Następnie utwórz kontekst dla przełączania motywów:
// components/theme-provider.tsx
"use client"
import { createContext, useContext, useEffect, useState } from "react"
type Theme = "light" | "dark" | "neon" | "vintage" | "system"
type ThemeProviderProps = {
children: React.ReactNode
defaultTheme?: Theme
storageKey?: string
}
type ThemeProviderState = {
theme: Theme
setTheme: (theme: Theme) => void
}
const initialState: ThemeProviderState = {
theme: "system",
setTheme: () => null,
}
const ThemeProviderContext = createContext<ThemeProviderState>(initialState)
export function ThemeProvider({
children,
defaultTheme = "system",
storageKey = "quantum-ui-theme",
...props
}: ThemeProviderProps) {
const [theme, setTheme] = useState<Theme>(
() => (localStorage.getItem(storageKey) as Theme) || defaultTheme
)
useEffect(() => {
const root = window.document.documentElement
// Usuń poprzednie klasy motywów
root.classList.remove("light", "dark", "theme-neon", "theme-vintage")
if (theme === "system") {
const systemTheme = window.matchMedia("(prefers-color-scheme: dark)")
.matches
? "dark"
: "light"
root.classList.add(systemTheme)
return
}
if (theme === "neon") {
root.classList.add("dark", "theme-neon")
return
}
if (theme === "vintage") {
root.classList.add("light", "theme-vintage")
return
}
root.classList.add(theme)
}, [theme])
const value = {
theme,
setTheme: (theme: Theme) => {
localStorage.setItem(storageKey, theme)
setTheme(theme)
},
}
return (
<ThemeProviderContext.Provider {...props} value={value}>
{children}
</ThemeProviderContext.Provider>
)
}
export const useTheme = () => {
const context = useContext(ThemeProviderContext)
if (context === undefined)
throw new Error("useTheme must be used within a ThemeProvider")
return context
}- Użyj dostawcy motywu w swoim layoucie:
// app/layout.tsx
import { ThemeProvider } from "@/components/theme-provider"
import "./globals.css"
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="pl" suppressHydrationWarning>
<body>
<ThemeProvider defaultTheme="system" storageKey="quantum-theme">
{children}
</ThemeProvider>
</body>
</html>
)
}Komponent przełącznika motywów
// components/theme-switcher.tsx
"use client"
import { Moon, Sun, Sparkles, Clock } from "lucide-react"
import { useTheme } from "@/components/theme-provider"
import { Button } from "@/components/ui/button"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
export function ThemeSwitcher() {
const { theme, setTheme } = useTheme()
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" size="icon">
<Sun className="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
<Moon className="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
<span className="sr-only">Przełącz motyw</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => setTheme("light")}>
<Sun className="mr-2 h-4 w-4" />
<span>Jasny</span>
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setTheme("dark")}>
<Moon className="mr-2 h-4 w-4" />
<span>Ciemny</span>
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setTheme("neon")}>
<Sparkles className="mr-2 h-4 w-4" />
<span>Neon</span>
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setTheme("vintage")}>
<Clock className="mr-2 h-4 w-4" />
<span>Vintage</span>
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setTheme("system")}>
<span>Systemowy</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)
}Najlepsze praktyki pracy z Shadcn/UI
1. Organizacja komponentów
Utrzymuj przejrzystą strukturę katalogów:
components/
├── ui/ # Podstawowe komponenty Shadcn/UI
│ ├── button.tsx
│ ├── card.tsx
│ └──
├── quantum/ # Twoje własne komponenty biznesowe
│ ├── quantum-button.tsx
│ ├── quantum-card.tsx
│ └──
└── blocks/ # Złożone bloki interfejsu
├── dashboard-stats.tsx
├── user-profile-form.tsx
└──2. Spójne nazewnictwo
Utrzymuj spójne konwencje nazewnictwa dla własnych komponentów:
Button- podstawowy komponent Shadcn/UIQuantumButton- własne rozszerzenie podstawowego komponentuUserProfileForm- złożony komponent biznesowy
3. Wykorzystanie generatorów komponentów
Używaj CLI Shadcn/UI do generowania nowych komponentów:
npx shadcn@latest add alert4. Optymalizacja wielokrotnego używania
Dla często używanych kombinacji, twórz własne komponenty wyższego poziomu:
// components/quantum/quantum-section.tsx
import { cn } from "@/lib/utils"
interface QuantumSectionProps {
title: string
description?: string
children: React.ReactNode
className?: string
}
export function QuantumSection({
title,
description,
children,
className,
}: QuantumSectionProps) {
return (
<section className={cn("space-y-4", className)}>
<div className="space-y-1">
<h2 className="text-2xl font-bold">{title}</h2>
{description && (
<p className="text-muted-foreground">{description}</p>
)}
</div>
<div>{children}</div>
</section>
)
}5. Zarządzanie motywami
Używaj zmiennych CSS, aby zarządzać motywami:
// lib/themes.ts
export const themes = {
default: {
primary: "222.2 47.4% 11.2%",
// ...inne kolory
},
quantum: {
primary: "240 100% 50%",
// ...inne kolory
},
// ...inne motywy
}
export function setTheme(theme: keyof typeof themes) {
const root = document.documentElement
Object.entries(themes[theme]).forEach(([key, value]) => {
root.style.setProperty(`--${key}`, value)
})
}Integracja z innymi narzędziami
1. Integracja z bibliotekami formularzy
Shadcn/UI dobrze integruje się z bibliotekami takimi jak React Hook Form i Zod:
npx shadcn@latest add form// components/signup-form.tsx
"use client"
import { zodResolver } from "@hookform/resolvers/zod"
import { useForm } from "react-hook-form"
import * as z from "zod"
import { Button } from "@/components/ui/button"
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form"
import { Input } from "@/components/ui/input"
const formSchema = z.object({
username: z.string().min(2).max(50),
email: z.string().email(),
password: z.string().min(8),
})
export function SignupForm() {
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
username: "",
email: "",
password: "",
},
})
function onSubmit(values: z.infer<typeof formSchema>) {
console.log(values)
}
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
<FormField
control={form.control}
name="username"
render={({ field }) => (
<FormItem>
<FormLabel>Nazwa użytkownika</FormLabel>
<FormControl>
<Input placeholder="Wpisz nazwę użytkownika" {...field} />
</FormControl>
<FormDescription>
Nazwa, która będzie wyświetlana w Twoim profilu.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
{/* Pozostałe pola formularza */}
<Button type="submit">Zarejestruj się</Button>
</form>
</Form>
)
}2. Integracja z bibliotekami animacji
Shadcn/UI można łatwo zintegrować z bibliotekami animacji:
// components/animated-card.tsx
"use client"
import { motion } from "framer-motion"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
interface AnimatedCardProps {
title: string
children: React.ReactNode
}
export function AnimatedCard({ title, children }: AnimatedCardProps) {
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }}
>
<Card>
<CardHeader>
<CardTitle>{title}</CardTitle>
</CardHeader>
<CardContent>{children}</CardContent>
</Card>
</motion.div>
)
}3. Integracja z zarządzaniem stanem
Shadcn/UI dobrze współpracuje z bibliotekami do zarządzania stanem:
// store/theme-store.ts
import { create } from 'zustand'
type Theme = 'light' | 'dark' | 'system'
interface ThemeState {
theme: Theme
setTheme: (theme: Theme) => void
}
export const useThemeStore = create<ThemeState>((set) => ({
theme: 'system',
setTheme: (theme) => set({ theme }),
}))
// components/theme-toggle.tsx
"use client"
import { Moon, Sun } from "lucide-react"
import { useThemeStore } from "@/store/theme-store"
import { Button } from "@/components/ui/button"
import { useEffect } from "react"
export function ThemeToggle() {
const { theme, setTheme } = useThemeStore()
useEffect(() => {
const root = document.documentElement
if (theme === "system") {
const systemTheme = window.matchMedia("(prefers-color-scheme: dark)")
.matches ? "dark" : "light"
root.classList.remove("light", "dark")
root.classList.add(systemTheme)
} else {
root.classList.remove("light", "dark")
root.classList.add(theme)
}
}, [theme])
return (
<Button
variant="outline"
size="icon"
onClick={() => setTheme(theme === "dark" ? "light" : "dark")}
>
<Sun className="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
<Moon className="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
<span className="sr-only">Przełącz motyw</span>
</Button>
)
}Podsumowanie
Shadcn/UI to potężne narzędzie do tworzenia pięknych, dostępnych i spójnych interfejsów użytkownika w Next.js 16. 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:
- Kontrola - komponenty są częścią twojego kodu, więc masz nad nimi pełną kontrolę
- Modularność - pobierasz tylko te komponenty, których potrzebujesz
- Stylizacja - łatwe dostosowanie wyglądu za pomocą Tailwind CSS
- Dostępność - komponenty zbudowane na Radix UI, które zapewniają wysoki poziom dostępności
- Wbudowany ciemny motyw - wsparcie dla jasnego i ciemnego motywu w standardzie
- 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 16, co pozwoli nam dalej poprawić wydajność i user experience naszych aplikacji.
Kod do tej lekcji: app/page.tsx
// Shadcn/UI Components Demo - Next.js 16
'use client';
import { useState } from 'react';
// Simplified Shadcn/UI-style components for demonstration
function Button({ variant = 'default', size = 'default', children, onClick, className = '' }) {
const baseStyles = 'inline-flex items-center justify-center rounded-md font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2';
const variants = {
default: 'bg-blue-600 text-white hover:bg-blue-700 focus:ring-blue-500',
destructive: 'bg-red-600 text-white hover:bg-red-700 focus:ring-red-500',
outline: 'border-2 border-gray-300 bg-transparent hover:bg-gray-100 focus:ring-gray-500',
ghost: 'hover:bg-gray-100 focus:ring-gray-500',
};
const sizes = {
default: 'h-10 px-4 py-2',
sm: 'h-8 px-3 text-sm',
lg: 'h-12 px-8 text-lg',
icon: 'h-10 w-10',
};
return (
<button
onClick={onClick}
className={`${baseStyles} ${variants[variant]} ${sizes[size]} ${className}`}
>
{children}
</button>
);
}
function Card({ title, description, children }) {
return (
<div className="rounded-lg border border-gray-200 bg-white shadow-sm">
<div className="p-6">
{title && <h3 className="text-lg font-semibold mb-1">{title}</h3>}
{description && <p className="text-sm text-gray-500 mb-4">{description}</p>}
{children}
</div>
</div>
);
}
function Badge({ variant = 'default', children }) {
const variants = {
default: 'bg-blue-100 text-blue-800',
success: 'bg-green-100 text-green-800',
warning: 'bg-yellow-100 text-yellow-800',
destructive: 'bg-red-100 text-red-800',
};
return (
<span className={`inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-semibold ${variants[variant]}`}>
{children}
</span>
);
}
export default function ShadcnDemo() {
const [count, setCount] = useState(0);
return (
<div className="min-h-screen bg-gray-50 p-8">
<div className="max-w-4xl mx-auto">
<h1 className="text-3xl font-bold mb-2">Shadcn/UI Components</h1>
<p className="text-gray-500 mb-8">Modular, customizable UI components for Next.js 16</p>
{/* Button Variants */}
<Card title="Button Variants" description="Different button styles for various actions">
<div className="flex flex-wrap gap-3">
<Button>Default</Button>
<Button variant="destructive">Destructive</Button>
<Button variant="outline">Outline</Button>
<Button variant="ghost">Ghost</Button>
</div>
<div className="flex flex-wrap gap-3 mt-4">
<Button size="sm">Small</Button>
<Button size="default">Default</Button>
<Button size="lg">Large</Button>
</div>
</Card>
{/* Badges */}
<Card title="Badges" description="Status indicators and labels" className="mt-6">
<div className="flex flex-wrap gap-2">
<Badge>Default</Badge>
<Badge variant="success">Success</Badge>
<Badge variant="warning">Warning</Badge>
<Badge variant="destructive">Error</Badge>
</div>
</Card>
{/* Interactive Counter */}
<Card title="Interactive Example" description="Components working together" className="mt-6">
<div className="flex items-center gap-4">
<Button variant="outline" onClick={() => setCount(c => c - 1)}>-</Button>
<span className="text-2xl font-bold w-12 text-center">{count}</span>
<Button variant="outline" onClick={() => setCount(c => c + 1)}>+</Button>
<Button variant="destructive" size="sm" onClick={() => setCount(0)}>
Reset
</Button>
</div>
</Card>
</div>
</div>
);
}Hasła z dokumentacji w tej lekcji
Widzisz błąd w tej lekcji?
Sprawdź się
Odpowiedz na pytania z tej lekcji. Wybierz odpowiedź, a od razu zobaczysz, czy jest poprawna.
1. Główną zaletą CSS Modules jest:
2. Tailwind CSS to:
Zadania praktyczne w grze
- Edytor kodu
Powtórzenie (temat z modułu 1): Zaimplementuj dashboard z responsywnym layoutem CSS
- Układanie w pionie
Uporządkuj selektory CSS od najniższej do najwyższej specyficzności:
- Klikanie w kolejności
Ułóż poprawną składnię deklaracji CSS Grid
- Edytor kodu
Utwórz komponent Button z własnym plikiem CSS Module zawierającym style dla primary i secondary button