Урок 18. Решения

📁 Раздел: shadcn/ui ⏱️ Время изучения: ~55 мин 🎯 Сложность: Продвинутая

⚡ Кратко

Разбор Badge, расширенная кнопка и готовый компонент Kbd по канону реестра.

Решение задания 1

У Badge шесть вариантов: default, secondary, destructive, outline, ghost, link; по умолчанию — default. Части помечены data-slot="badge". Конструкция [&>svg]:size-3! задаёт размер прямым потомкам-иконкам, а восклицательный знак делает правило важным (!important), чтобы перебить размер, заданный самой иконкой.

Модификатор ! в конце класса — крайняя мера. В компонентах реестра он встречается там, где иконки приходят из внешней библиотеки со своими размерами. В своём коде старайтесь обходиться без него.
default secondary destructive outline
<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
// 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.