Урок 21. Примеры: диалоги, панели, меню, вкладки

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

⚡ Кратко

Пять оверлеев: подтверждение удаления, диалог с формой, боковая панель, меню строки таблицы и вкладки настроек.

1. Подтверждение удаления

// src/components/delete-report-dialog.tsx
import {
  Dialog, DialogClose, DialogContent, DialogDescription,
  DialogFooter, DialogHeader, DialogTitle, DialogTrigger,
} from "@/components/ui/dialog"
import { Button } from "@/components/ui/button"

export function DeleteReportDialog() {
  return (
    <Dialog>
      <DialogTrigger render={<Button variant="destructive">Удалить отчёт</Button>} />
      <DialogContent>
        <DialogHeader>
          <DialogTitle>Удалить отчёт?</DialogTitle>
          <DialogDescription>
            Действие нельзя отменить. Отчёт и его выгрузки будут удалены.
          </DialogDescription>
        </DialogHeader>
        <DialogFooter>
          <DialogClose render={<Button variant="ghost">Отмена</Button>} />
          <Button variant="destructive" onClick={handleDelete}>Удалить</Button>
        </DialogFooter>
      </DialogContent>
    </Dialog>
  )
}
Список отчётов…

Удалить отчёт?

Действие нельзя отменить. Отчёт и его выгрузки будут удалены.

<div class="relative h-52 overflow-hidden rounded-xl bg-muted">
  <div class="p-4 text-sm text-muted-foreground">Список отчётов…</div>
  <div class="absolute inset-0 z-40 bg-black/10 backdrop-blur-xs"></div>
  <div class="absolute top-1/2 left-1/2 z-50 grid w-[calc(100%-2rem)] max-w-sm -translate-x-1/2 -translate-y-1/2 gap-4 rounded-xl bg-popover p-4 text-popover-foreground ring-1 ring-foreground/10">
    <div class="grid gap-1.5">
      <h3 class="text-base leading-snug font-medium">Удалить отчёт?</h3>
      <p class="text-sm text-muted-foreground">Действие нельзя отменить. Отчёт и его выгрузки будут удалены.</p>
    </div>
    <div class="flex justify-end gap-2">
      <button class="inline-flex h-8 items-center justify-center rounded-lg px-3 text-sm font-medium hover:bg-muted">Отмена</button>
      <button class="inline-flex h-8 items-center justify-center rounded-lg bg-destructive/10 px-3 text-sm font-medium text-destructive hover:bg-destructive/20">Удалить</button>
    </div>
  </div>
</div>

Опасное действие — справа, отмена — слева и менее заметная. Заголовок сформулирован вопросом, описание объясняет последствия.

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

<Dialog>
  <DialogTrigger render={<Button>Пригласить участника</Button>} />
  <DialogContent>
    <DialogHeader>
      <DialogTitle>Пригласить участника</DialogTitle>
      <DialogDescription>Отправим письмо со ссылкой на вступление.</DialogDescription>
    </DialogHeader>
    <form id="invite" onSubmit={handleSubmit} className="grid gap-3">
      <Field>
        <FieldLabel htmlFor="invite-email">Почта</FieldLabel>
        <Input id="invite-email" type="email" autoComplete="email" required />
      </Field>
    </form>
    <DialogFooter>
      <DialogClose render={<Button variant="ghost">Отмена</Button>} />
      <Button type="submit" form="invite">Отправить</Button>
    </DialogFooter>
  </DialogContent>
</Dialog>

Пригласить участника

Отправим письмо со ссылкой на вступление.

<div class="w-full max-w-sm rounded-xl bg-popover p-4 text-popover-foreground ring-1 ring-foreground/10">
  <div class="grid gap-1.5">
    <h3 class="text-base leading-snug font-medium">Пригласить участника</h3>
    <p class="text-sm text-muted-foreground">Отправим письмо со ссылкой на вступление.</p>
  </div>
  <div class="mt-4 grid gap-1.5">
    <label class="text-sm font-medium" for="ov-invite">Почта</label>
    <input id="ov-invite" type="email" placeholder="name@example.com"
           class="h-8 w-full rounded-lg border border-input bg-transparent px-2.5 text-sm outline-none placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50">
  </div>
  <div class="mt-4 flex justify-end gap-2">
    <button class="inline-flex h-8 items-center justify-center rounded-lg 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>

Приём с form="invite": кнопка лежит в футере диалога, вне тега <form>, но отправляет именно его.

3. Боковая панель фильтров

<Sheet>
  <SheetTrigger render={<Button variant="outline">Фильтры</Button>} />
  <SheetContent side="right">
    <SheetHeader>
      <SheetTitle>Фильтры</SheetTitle>
      <SheetDescription>Уточните выборку отчётов</SheetDescription>
    </SheetHeader>
    <div className="grid gap-3 p-4">…</div>
    <SheetFooter>
      <Button className="w-full">Применить</Button>
    </SheetFooter>
  </SheetContent>
</Sheet>
Таблица отчётов…
<div class="relative h-56 overflow-hidden rounded-xl bg-muted">
  <div class="p-4 text-sm text-muted-foreground">Таблица отчётов…</div>
  <div class="absolute inset-0 z-40 bg-black/10"></div>
  <aside class="absolute inset-y-0 right-0 z-50 flex w-72 flex-col gap-4 bg-popover p-4 text-popover-foreground ring-1 ring-foreground/10">
    <div class="grid gap-1">
      <h3 class="text-base font-medium">Фильтры</h3>
      <p class="text-sm text-muted-foreground">Уточните выборку отчётов</p>
    </div>
    <div class="grid gap-2 text-sm">
      <label class="flex items-center gap-2"><input type="checkbox" checked class="size-4 accent-primary"> Только мои</label>
      <label class="flex items-center gap-2"><input type="checkbox" class="size-4 accent-primary"> С вложениями</label>
      <label class="flex items-center gap-2"><input type="checkbox" class="size-4 accent-primary"> Архивные</label>
    </div>
    <div class="mt-auto flex gap-2">
      <button class="inline-flex h-8 flex-1 items-center justify-center rounded-lg px-3 text-sm font-medium hover:bg-muted">Сбросить</button>
      <button class="inline-flex h-8 flex-1 items-center justify-center rounded-lg bg-primary px-3 text-sm font-medium text-primary-foreground">Применить</button>
    </div>
  </aside>
</div>

4. Меню действий в строке таблицы

<DropdownMenu>
  <DropdownMenuTrigger render={<Button variant="ghost" size="icon-sm" aria-label="Действия" />}>
    ⋯
  </DropdownMenuTrigger>
  <DropdownMenuContent align="end">
    <DropdownMenuLabel>Отчёт № 1042</DropdownMenuLabel>
    <DropdownMenuItem>Открыть</DropdownMenuItem>
    <DropdownMenuItem>Скачать PDF</DropdownMenuItem>
    <DropdownMenuSeparator />
    <DropdownMenuItem variant="destructive">Удалить</DropdownMenuItem>
  </DropdownMenuContent>
</DropdownMenu>

Отчёт № 1042

Обновлён вчера

Отчёт № 1042
<div class="flex w-full max-w-lg items-start justify-between gap-4">
  <div class="flex-1 rounded-lg bg-card p-3 text-sm text-card-foreground ring-1 ring-border">
    <p class="font-medium">Отчёт № 1042</p>
    <p class="text-muted-foreground">Обновлён вчера</p>
  </div>
  <div class="w-52 rounded-xl bg-popover p-1 text-sm text-popover-foreground ring-1 ring-foreground/10">
    <div class="px-2 py-1.5 text-xs text-muted-foreground">Отчёт № 1042</div>
    <button class="w-full rounded-lg px-2 py-1.5 text-left hover:bg-muted">Открыть</button>
    <button class="w-full rounded-lg px-2 py-1.5 text-left hover:bg-muted">Скачать PDF</button>
    <div class="my-1 h-px bg-border"></div>
    <button class="w-full rounded-lg px-2 py-1.5 text-left text-destructive hover:bg-destructive/10">Удалить</button>
  </div>
</div>

5. Вкладки настроек

<Tabs defaultValue="profile">
  <TabsList>
    <TabsTrigger value="profile">Профиль</TabsTrigger>
    <TabsTrigger value="security">Безопасность</TabsTrigger>
    <TabsTrigger value="billing">Оплата</TabsTrigger>
  </TabsList>
  <TabsContent value="profile">…</TabsContent>
  <TabsContent value="security">…</TabsContent>
  <TabsContent value="billing">…</TabsContent>
</Tabs>

Профиль

Имя, фото и контактные данные.

<div class="w-full max-w-lg">
  <div class="inline-flex h-8 w-fit 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 hover:text-foreground">Безопасность</button>
    <button class="inline-flex h-full items-center justify-center rounded-md px-3 text-sm font-medium hover:text-foreground">Оплата</button>
  </div>
  <div class="mt-3 rounded-xl bg-card p-4 text-sm text-card-foreground ring-1 ring-border">
    <p class="font-medium">Профиль</p>
    <p class="mt-1 text-muted-foreground">Имя, фото и контактные данные.</p>
  </div>
</div>

6. Вкладки-подчёркивания

Вариант оформления TabsList: line.
<div class="w-full max-w-lg">
  <div class="flex gap-1 border-b border-border">
    <button class="border-b-2 border-primary px-3 pb-2 text-sm font-medium text-foreground">Обзор</button>
    <button class="border-b-2 border-transparent px-3 pb-2 text-sm font-medium text-muted-foreground hover:border-border hover:text-foreground">Активность</button>
    <button class="border-b-2 border-transparent px-3 pb-2 text-sm font-medium text-muted-foreground hover:border-border hover:text-foreground">Настройки</button>
  </div>
  <div class="pt-3 text-sm text-muted-foreground">Вариант оформления TabsList: line.</div>
</div>