Урок 15. Домашнее задание: мини-набор компонентов

📁 Раздел: Организация кода ⏱️ Время изучения: ~55 мин 🎯 Сложность: Средняя

⚡ Кратко

Собрать мини-набор компонентов проекта: Button, Badge, Input, Card — с вариантами, функцией cn() и сортировкой классов.

Задание

В проекте курса создайте папку src/components/ui и перенесите в неё повторяющиеся элементы:

  • Button: варианты default, outline, ghost, destructive; размеры sm, md, lg;
  • Badge: четыре тона;
  • Input: состояние ошибки через aria-invalid;
  • Card из подкомпонентов: CardHeader, CardContent, CardFooter;
  • функция cn() в src/lib/utils.ts;
  • настроенный prettier-plugin-tailwindcss.

Подготовка окружения

cd tailwind-course
npm install clsx tailwind-merge class-variance-authority
npm install -D prettier prettier-plugin-tailwindcss

Пошаговое решение

Шаг 1. Функция cn

📄 src/lib/utils.ts
// src/lib/utils.ts
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"

export function cn(...inputs: ClassValue[]) {
  return twMerge(clsx(inputs))
}

Шаг 2. Button

📄 src/components/ui/button.tsx
// src/components/ui/button.tsx
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"

const buttonVariants = cva(
  "inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors " +
    "focus-visible:outline-2 focus-visible:outline-offset-2 disabled:pointer-events-none disabled:opacity-50",
  {
    variants: {
      variant: {
        default: "bg-slate-900 text-white hover:bg-slate-700",
        outline: "border border-slate-300 bg-white text-slate-700 hover:bg-slate-50",
        ghost: "text-slate-700 hover:bg-slate-100",
        destructive: "bg-rose-600 text-white hover:bg-rose-700",
      },
      size: {
        sm: "h-8 px-3 text-xs",
        md: "h-9 px-4",
        lg: "h-11 px-6 text-base",
      },
    },
    defaultVariants: { variant: "default", size: "md" },
  }
)

type ButtonProps = React.ComponentProps<"button"> & VariantProps<typeof buttonVariants>

export function Button({ className, variant, size, ...props }: ButtonProps) {
  return <button className={cn(buttonVariants({ variant, size }), className)} {...props} />
}

Проверьте: <Button className="w-full"> должен растягиваться, а <Button className="px-10"> — переопределять внутренний отступ.

Шаг 3. Card из частей

📄 src/components/ui/card.tsx
// src/components/ui/card.tsx
import { cn } from "@/lib/utils"

export function Card({ className, ...props }: React.ComponentProps<"div">) {
  return <div className={cn("rounded-xl border border-slate-200 bg-white", className)} {...props} />
}

export function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
  return <div className={cn("border-b border-slate-200 px-4 py-3", className)} {...props} />
}

export function CardContent({ className, ...props }: React.ComponentProps<"div">) {
  return <div className={cn("px-4 py-3", className)} {...props} />
}

export function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
  return <div className={cn("flex justify-end gap-2 border-t border-slate-200 px-4 py-3", className)} {...props} />
}

Шаг 4. Замена в страницах

Пройдитесь по страницам проекта и замените повторяющуюся разметку на новые компоненты. Строки классов должны заметно укоротиться.

Как проверить

  • В страницах не осталось длинных строк классов для кнопок и бейджей.
  • Внешний className переопределяет базовые классы.
  • Prettier сортирует классы при сохранении файла.
  • TypeScript подсказывает доступные варианты компонента.

Ориентир

Отчёт за июль

Готов
Карточка собрана из компонентов Card, CardHeader, CardContent и CardFooter.
<div class="max-w-md rounded-xl border border-slate-200 bg-white">
  <div class="flex items-center justify-between border-b border-slate-200 px-4 py-3">
    <h3 class="text-sm font-semibold text-slate-900">Отчёт за июль</h3>
    <span class="inline-flex items-center rounded-full bg-emerald-100 px-2.5 py-0.5 text-xs font-medium text-emerald-800">Готов</span>
  </div>
  <div class="px-4 py-3 text-sm text-slate-600">
    Карточка собрана из компонентов Card, CardHeader, CardContent и CardFooter.
  </div>
  <div class="flex justify-end gap-2 border-t border-slate-200 px-4 py-3">
    <button class="inline-flex h-9 items-center justify-center rounded-md border border-slate-300 bg-white px-4 text-sm font-medium text-slate-700 hover:bg-slate-50">Отмена</button>
    <button class="inline-flex h-9 items-center justify-center rounded-md bg-slate-900 px-4 text-sm font-medium text-white hover:bg-slate-700">Открыть</button>
  </div>
</div>