Kurs Next.js · Moduł 3: Stylowanie i optymalizacja wizualna

Shadcn/UI - Komponenty dla Next.js 16

21 min czytania ·
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:

  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 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 init

Po 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:

  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

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 button

To 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

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

  1. 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 */
}
  1. 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
}
  1. 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

  1. 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%;

    /* ... */
  }
}
  1. 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
}
  1. 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/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:

npx shadcn@latest add alert

4. 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:

  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 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. 1. Główną zaletą CSS Modules jest:

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

Przydatne artykuły