Урок 18. Решения
⚡ Кратко
Разбор Badge, расширенная кнопка и готовый компонент Kbd по канону реестра.
Решение задания 1
У Badge шесть вариантов: default,
secondary, destructive, outline,
ghost, link; по умолчанию — default.
Части помечены data-slot="badge". Конструкция
[&>svg]:size-3! задаёт размер прямым потомкам-иконкам,
а восклицательный знак делает правило важным (!important),
чтобы перебить размер, заданный самой иконкой.
! в конце класса —
крайняя мера. В компонентах реестра он встречается там, где иконки
приходят из внешней библиотеки со своими размерами. В своём коде
старайтесь обходиться без него.<div class="flex flex-wrap items-center gap-2">
<span class="inline-flex h-5 items-center rounded-4xl bg-primary px-2 text-xs font-medium text-primary-foreground">default</span>
<span class="inline-flex h-5 items-center rounded-4xl bg-secondary px-2 text-xs font-medium text-secondary-foreground">secondary</span>
<span class="inline-flex h-5 items-center rounded-4xl bg-destructive/10 px-2 text-xs font-medium text-destructive">destructive</span>
<span class="inline-flex h-5 items-center rounded-4xl border border-border px-2 text-xs font-medium text-foreground">outline</span>
</div>
Решение задания 2
// src/components/ui/button.tsx
const buttonVariants = cva("…базовые классы…", {
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/80",
outline: "border-border bg-background hover:bg-muted",
ghost: "hover:bg-muted hover:text-foreground",
destructive: "bg-destructive/10 text-destructive hover:bg-destructive/20",
link: "text-primary underline-offset-4 hover:underline",
brand: "bg-brand text-brand-foreground hover:opacity-90", // ←
},
size: {
default: "h-8 gap-1.5 px-2.5",
sm: "h-7 gap-1 px-2.5 text-[0.8rem]",
lg: "h-9 gap-1.5 px-2.5",
xl: "h-11 gap-2 px-5 text-base [&_svg:not([class*='size-'])]:size-5", // ←
icon: "size-8",
},
},
defaultVariants: { variant: "default", size: "default" },
})
<div class="flex flex-wrap items-center gap-3">
<button class="inline-flex h-11 items-center justify-center gap-2 rounded-lg bg-brand px-5 text-base font-medium text-brand-foreground hover:opacity-90">variant=brand size=xl</button>
<button class="inline-flex h-8 items-center justify-center gap-1.5 rounded-lg bg-brand px-2.5 text-sm font-medium text-brand-foreground hover:opacity-90">brand default</button>
</div>
Проверка переопределения:
<Button className="px-10"> должен дать отступ 10, а не 2.5 —
за это отвечает cn() внутри компонента.
Решение задания 3
// src/components/ui/kbd.tsx
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const kbdVariants = cva(
"inline-flex select-none items-center justify-center rounded-md border border-border " +
"bg-muted font-medium text-muted-foreground",
{
variants: {
size: {
sm: "h-5 min-w-5 px-1 text-[10px]",
md: "h-6 min-w-6 px-1.5 text-xs",
},
},
defaultVariants: { size: "md" },
}
)
type KbdProps = React.ComponentProps<"kbd"> & VariantProps<typeof kbdVariants>
export function Kbd({ className, size, ...props }: KbdProps) {
return <kbd data-slot="kbd" className={cn(kbdVariants({ size }), className)} {...props} />
}
<span className="text-sm text-muted-foreground">
Нажмите <Kbd>Ctrl</Kbd> + <Kbd>K</Kbd> для поиска
</span>
Нажмите Ctrl + K для поиска
<p class="flex items-center gap-1.5 text-sm text-muted-foreground">
Нажмите
<span class="inline-flex h-6 min-w-6 items-center justify-center rounded-md border border-border bg-muted px-1.5 text-xs font-medium text-muted-foreground">Ctrl</span>
+
<span class="inline-flex h-6 min-w-6 items-center justify-center rounded-md border border-border bg-muted px-1.5 text-xs font-medium text-muted-foreground">K</span>
для поиска
</p>
Что здесь по канону: семантический тег
<kbd>, метка data-slot, варианты через
cva, внешний className последним аргументом
cn(), типы выведены из VariantProps.