Урок 18. Домашнее задание: компоненты по канону
⚡ Кратко
Написать два собственных компонента по канону реестра —
Kbd и Alert — и расширить Button
фирменным вариантом.
Задание
- расширить
Button: вариантbrandи размерxl; - написать
Kbdс двумя размерами; - написать
Alertиз подкомпонентов (Alert,AlertTitle,AlertDescription) с вариантамиinfo,success,warning,danger; - все компоненты:
data-slot,cva,cn(), семантические токены вместо конкретных цветов; - проверить работу в обеих темах.
Подготовка окружения
cd tailwind-course
npm run dev
Пошаговое решение
Шаг 1. Изучите образец
Откройте button.tsx и держите его рядом: ваши компоненты
должны выглядеть так же по структуре — импорты, cva,
функция, экспорт.
Шаг 2. Kbd
Самый простой: один тег, два размера. Хорошая тренировка канона без отвлечения на логику.
Шаг 3. Alert из частей
📄 src/components/ui/alert.tsx
// src/components/ui/alert.tsx
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const alertVariants = cva(
"relative grid w-full grid-cols-[auto_1fr] items-start gap-x-3 gap-y-1 rounded-lg border p-4 text-sm " +
"[&_svg]:size-4 [&_svg]:translate-y-0.5",
{
variants: {
variant: {
info: "border-border bg-card text-card-foreground",
success: "border-emerald-500/30 bg-emerald-500/10 text-emerald-800 dark:text-emerald-300",
warning: "border-amber-500/30 bg-amber-500/10 text-amber-800 dark:text-amber-300",
danger: "border-destructive/30 bg-destructive/10 text-destructive",
},
},
defaultVariants: { variant: "info" },
}
)
export function Alert({ className, variant, ...props }: React.ComponentProps<"div"> & VariantProps<typeof alertVariants>) {
return <div data-slot="alert" role="alert" className={cn(alertVariants({ variant }), className)} {...props} />
}
export function AlertTitle({ className, ...props }: React.ComponentProps<"div">) {
return <div data-slot="alert-title" className={cn("col-start-2 font-medium", className)} {...props} />
}
export function AlertDescription({ className, ...props }: React.ComponentProps<"div">) {
return <div data-slot="alert-description" className={cn("col-start-2 text-sm opacity-90", className)} {...props} />
}
Обратите внимание на role="alert": сообщение будет
объявлено скринридером при появлении.
Шаг 4. Проверка в двух темах
Переключите тему и убедитесь, что все варианты читаемы. Если пришлось
добавлять dark: — подумайте, нельзя ли выразить цвет через
прозрачность (bg-emerald-500/10), которая работает в обеих темах.
Как проверить
- Каждый компонент принимает
classNameи корректно его сливает. - У каждой части есть
data-slot. - TypeScript подсказывает варианты.
- В обеих темах контраст текста достаточный.
Ориентир
ℹ️
Обновление доступно
Версия 4.3 уже вышла.
✅
Сохранено
Изменения применены.
⛔
Не удалось сохранить
Проверьте соединение и повторите.
<div class="space-y-3">
<div role="alert" class="grid w-full grid-cols-[auto_1fr] items-start gap-x-3 gap-y-1 rounded-lg border border-border bg-card p-4 text-sm text-card-foreground">
<span class="translate-y-0.5">ℹ️</span>
<div class="col-start-2 font-medium">Обновление доступно</div>
<div class="col-start-2 text-sm opacity-90">Версия 4.3 уже вышла.</div>
</div>
<div role="alert" class="grid w-full grid-cols-[auto_1fr] items-start gap-x-3 gap-y-1 rounded-lg border border-emerald-500/30 bg-emerald-500/10 p-4 text-sm text-emerald-800 dark:text-emerald-300">
<span class="translate-y-0.5">✅</span>
<div class="col-start-2 font-medium">Сохранено</div>
<div class="col-start-2 text-sm opacity-90">Изменения применены.</div>
</div>
<div role="alert" class="grid w-full grid-cols-[auto_1fr] items-start gap-x-3 gap-y-1 rounded-lg border border-destructive/30 bg-destructive/10 p-4 text-sm text-destructive">
<span class="translate-y-0.5">⛔</span>
<div class="col-start-2 font-medium">Не удалось сохранить</div>
<div class="col-start-2 text-sm opacity-90">Проверьте соединение и повторите.</div>
</div>
</div>