Урок 21. Домашнее задание: оверлеи в проекте

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

⚡ Кратко

Добавить в проект курса три оверлея: диалог создания записи, меню действий в списке и боковую панель фильтров — с проверкой клавиатурой.

Задание

  • диалог «Новая запись» с формой из урока 20 внутри;
  • меню действий у каждой строки списка: открыть, дублировать, удалить (с подтверждением);
  • боковая панель фильтров, открывающаяся кнопкой в шапке;
  • вкладки «Активные / Архив» над списком;
  • проверка доступности каждого оверлея.

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

cd tailwind-course
npx shadcn@latest add dialog sheet dropdown-menu tabs
npm run dev

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

Шаг 1. Диалог с формой

Положите форму внутрь DialogContent, а кнопку отправки — в DialogFooter с атрибутом form. После успешной отправки закрывайте окно управляемым состоянием (open / onOpenChange).

const [open, setOpen] = useState(false)

const onSubmit = async (values: Values) => {
  await api.create(values)
  setOpen(false)
}

<Dialog open={open} onOpenChange={setOpen}>…</Dialog>

Шаг 2. Меню и подтверждение

Пункт «Удалить» открывает AlertDialog. Помните про вложенность: закройте меню перед открытием подтверждения — примитивы обычно делают это сами, но проверьте поведение.

Шаг 3. Панель фильтров

На мобильном — Sheet снизу или справа, на десктопе можно показывать фильтры прямо в раскладке. Условие переключения — обычный адаптивный класс, а не отдельная страница.

Шаг 4. Проверка клавиатурой

Для каждого оверлея: открытие с клавиатуры, фокус внутри, обход по кругу, Esc, возврат фокуса. Запишите результат — это и есть отчёт по заданию.

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

  • У каждого диалога есть заголовок (видимый или sr-only).
  • Опасные действия требуют подтверждения и названы глаголом.
  • Фокус после закрытия возвращается на триггер.
  • Длинное содержимое прокручивается внутри окна.
  • Всё работает в тёмной теме.

Ориентир

  • Отчёт по продажам за июль
  • Смета на оборудование
<div class="w-full max-w-2xl space-y-3">
  <div class="flex items-center justify-between">
    <div class="inline-flex h-8 items-center justify-center rounded-lg bg-muted p-[3px] text-muted-foreground">
      <button class="inline-flex h-full items-center justify-center rounded-md bg-background px-3 text-sm font-medium text-foreground shadow-sm">Активные</button>
      <button class="inline-flex h-full items-center justify-center rounded-md px-3 text-sm font-medium">Архив</button>
    </div>
    <div class="flex gap-2">
      <button class="inline-flex h-8 items-center justify-center rounded-lg border border-border bg-background px-3 text-sm font-medium hover:bg-muted">Фильтры</button>
      <button class="inline-flex h-8 items-center justify-center rounded-lg bg-primary px-3 text-sm font-medium text-primary-foreground">Новая запись</button>
    </div>
  </div>
  <ul class="divide-y divide-border rounded-xl bg-card text-sm text-card-foreground ring-1 ring-border">
    <li class="flex items-center justify-between gap-3 p-3">
      <span class="min-w-0 truncate">Отчёт по продажам за июль</span>
      <button aria-label="Действия" class="inline-flex size-7 shrink-0 items-center justify-center rounded-lg text-muted-foreground hover:bg-muted hover:text-foreground">⋯</button>
    </li>
    <li class="flex items-center justify-between gap-3 p-3">
      <span class="min-w-0 truncate">Смета на оборудование</span>
      <button aria-label="Действия" class="inline-flex size-7 shrink-0 items-center justify-center rounded-lg text-muted-foreground hover:bg-muted hover:text-foreground">⋯</button>
    </li>
  </ul>
</div>