# Alert

## Кратко

`Alert` показывает короткие сообщения обратной связи или статуса с необязательными иконкой, заголовком и текстом. Это поверхность обратной связи уровня сообщения: когда `Badge` слишком мал, а `Card` слишком нейтральна.

## Импорт

```tsx
import {
  Alert,
  AlertIcon,
  AlertMain,
  AlertTitle,
  AlertContent,
  type AlertProps,
} from '@vega-ui/react';
```

## Экспортируемые типы

- `AlertProps`
- `AlertIconProps`
- `AlertMainProps`
- `AlertTitleProps`
- `AlertContentProps`

## Минимальная композиция

```tsx
import {
  Alert,
  AlertContent,
  AlertIcon,
  AlertMain,
  AlertTitle,
} from '@vega-ui/react';

export default function MinimalComposition() {
  return (
    <Alert variant='success' style={{ width: '100%' }}>
      <AlertIcon />
      <AlertMain>
        <AlertTitle>Saved</AlertTitle>
        <AlertContent>Profile updated successfully.</AlertContent>
      </AlertMain>
    </Alert>
  );
}
```

## Обязательные части

- `Alert`: корневая поверхность обратной связи
- `AlertMain`: обёртка основного текста

## Необязательные части

- `AlertIcon`: поверхность семантической иконки
- `AlertTitle`: более выраженная визуальная иерархия
- `AlertContent`: поясняющий текст сообщения

## Порядок композиции

1. `Alert`
2. `AlertIcon`
3. `AlertMain`
4. `AlertTitle`
5. `AlertContent`

## Варианты

- Семантические варианты: `success`, `error`, `warning`, `info`
- Внешний вид: `fill`, `surface`
- Композиционные варианты: с иконкой или без, с заголовком или без

## Пограничные случаи

- Используйте заголовок, только когда алерту нужна более сильная иерархия.
- Держите текст коротким; длинному содержимому место в другом компоненте.
- Подбирайте вариант алерта по смыслу, а не только по цвету.
- Если содержимое становится пошаговой инструкцией, многоэтапным или сильно интерактивным, алерт обычно уже не подходит.

## Частые ошибки

- Использование алертов как постоянного макета страницы вместо обратной связи.
- Передача серьёзности только цветом или иконкой.
- Размещение сложных сценариев в теле алерта.

## Примеры

### Базовый

#### Базовый: четыре варианта

Каждый вариант получает подходящую иконку по умолчанию из `AlertIcon`. `appearance='surface'` добавляет рамку для сообщений, которым нужен больший вес, как у отклонённой карты. Эти алерты присутствуют на странице с самого начала, поэтому роль живой области им не нужна.

```tsx
import { Alert, AlertContent, AlertIcon, AlertMain, AlertTitle } from '@vega-ui/react';

export default function Variants() {
  return (
    <div style={{ display: 'grid', gap: 'var(--spacing-6)', width: '100%', maxWidth: 560 }}>
      <Alert variant='info'>
        <AlertIcon />
        <AlertMain>
          <AlertTitle>Scheduled maintenance</AlertTitle>
          <AlertContent>The dashboard will be read-only on Sunday from 02:00 to 03:00 UTC.</AlertContent>
        </AlertMain>
      </Alert>
      <Alert variant='success'>
        <AlertIcon />
        <AlertMain>
          <AlertTitle>Domain verified</AlertTitle>
          <AlertContent>Emails from billing@northwind.io will now pass SPF and DKIM checks.</AlertContent>
        </AlertMain>
      </Alert>
      <Alert variant='warning'>
        <AlertIcon />
        <AlertMain>
          <AlertTitle>Storage almost full</AlertTitle>
          <AlertContent>You have used 46.2 GB of 50 GB. Uploads stop when the limit is reached.</AlertContent>
        </AlertMain>
      </Alert>
      <Alert variant='error' appearance='surface'>
        <AlertIcon />
        <AlertMain>
          <AlertTitle>Card declined</AlertTitle>
          <AlertContent>The payment for invoice INV-2048 was declined by the bank. Update the card to keep your plan.</AlertContent>
        </AlertMain>
      </Alert>
    </div>
  );
}
```

### Контролируемый/с состоянием

#### Контролируемый/с состоянием: объявление, которое можно закрыть

Видимостью владеет родитель. У кнопки закрытия своё доступное имя, а после закрытия фокус переходит на заголовок раздела, чтобы пользователь клавиатуры не потерял место на странице.

```tsx
import { useRef, useState } from 'react';
import type { CSSProperties } from 'react';
import { Alert, AlertContent, AlertIcon, AlertMain, AlertTitle, Button, Heading, Icon, IconButton } from '@vega-ui/react';
import { XIcon } from '@vega-ui/icons';

export default function Dismissible() {
  const [visible, setVisible] = useState(true);
  const headingRef = useRef<HTMLHeadingElement>(null);

  const dismiss = () => {
    setVisible(false);
    headingRef.current?.focus();
  };

  return (
    <section aria-labelledby='billing-heading' style={{ display: 'grid', gap: 'var(--spacing-6)', width: '100%', maxWidth: 560 }}>
      <Heading ref={headingRef} id='billing-heading' as='h2' size={4} fontWeight={600} tabIndex={-1}>Billing</Heading>
      {visible && (
        <Alert variant='info'>
          <AlertIcon />
          <AlertMain>
            <AlertTitle>New invoice layout</AlertTitle>
            <AlertContent>Invoices now group usage by project. Old invoices keep their original layout.</AlertContent>
          </AlertMain>
          <IconButton
            aria-label='Dismiss'
            variant='secondary'
            appearance='ghost'
            onClick={dismiss}
            style={{ '--icon-button-size': '24px', '--icon-button-br-ratio': 0.5 } as CSSProperties}
          >
            <Icon size='xs'><XIcon strokeWidth={2.5} /></Icon>
          </IconButton>
        </Alert>
      )}
      {!visible && (
        <Button size='sm' variant='secondary' appearance='ghost' onClick={() => setVisible(true)} style={{ justifySelf: 'start' }}>
          Show the announcement again
        </Button>
      )}
    </section>
  );
}
```

### Форма/интеграция

#### Форма/интеграция: результат сохранения после отправки

Форма читает значения через `FormData` и имитирует запрос. Успех рендерится в контейнер `role='status'`, который существует до появления сообщения, поэтому скринридер его озвучивает; ошибка — это настойчивый `role='alert'`.

```tsx
import { FormEvent, useState } from 'react';
import {
  Alert,
  AlertContent,
  AlertIcon,
  AlertMain,
  AlertTitle,
  Button,
  Checkbox,
  CheckboxCheckedIcon,
  CheckboxIndicator,
  CheckboxInput,
  Label,
  TextField,
  TextFieldInput,
} from '@vega-ui/react';

type Status = 'idle' | 'saving' | 'saved' | 'failed';

export default function SaveFeedback() {
  const [status, setStatus] = useState<Status>('idle');
  const [savedName, setSavedName] = useState('');

  const submit = (event: FormEvent<HTMLFormElement>) => {
    event.preventDefault();
    const data = new FormData(event.currentTarget);
    const name = String(data.get('workspace'));
    const offline = data.get('offline') === 'on';
    setStatus('saving');
    setTimeout(() => {
      if (offline) {
        setStatus('failed');
        return;
      }
      setSavedName(name);
      setStatus('saved');
    }, 800);
  };

  return (
    <form onSubmit={submit} style={{ display: 'grid', gap: 'var(--spacing-12)', width: '100%', maxWidth: 420 }}>
      <div style={{ display: 'grid', gap: 'var(--spacing-8)' }}>
        <div style={{ display: 'grid', gap: 'var(--spacing-3)' }}>
          <Label htmlFor='workspace-name'>Workspace name</Label>
          <TextField>
            <TextFieldInput id='workspace-name' name='workspace' defaultValue='Northwind Studio' required />
          </TextField>
        </div>
        <Checkbox style={{ display: 'flex', alignItems: 'center', gap: 'var(--spacing-4)' }}>
          <CheckboxInput name='offline' />
          <CheckboxIndicator>
            <CheckboxCheckedIcon />
          </CheckboxIndicator>
          Simulate a network error
        </Checkbox>
      </div>
      <Button type='submit' disabled={status === 'saving'} style={{ justifySelf: 'start' }}>
        {status === 'saving' ? 'Saving…' : 'Save'}
      </Button>
      <div style={{ display: 'grid' }}>
        <div role='status'>
          {status === 'saved' && (
            <Alert variant='success'>
              <AlertIcon />
              <AlertMain>
                <AlertContent>Workspace renamed to “{savedName}”.</AlertContent>
              </AlertMain>
            </Alert>
          )}
        </div>
        {status === 'failed' && (
          <Alert variant='error' role='alert'>
            <AlertIcon />
            <AlertMain>
              <AlertTitle>Changes not saved</AlertTitle>
              <AlertContent>We could not reach the server. Check your connection and save again.</AlertContent>
            </AlertMain>
          </Alert>
        )}
      </div>
    </form>
  );
}
```

### Раскладка/оверлей

#### Раскладка/оверлей: предупреждение в диалоге подтверждения

Предупреждение объясняет последствия разрушительного действия и связано с диалогом через `aria-describedby`, поэтому зачитывается вместе с заголовком диалога.

```tsx
import type { CSSProperties } from 'react';
import {
  Alert,
  AlertContent,
  AlertIcon,
  AlertMain,
  AlertTitle,
  Button,
  Dialog,
  DialogClose,
  DialogContent,
  DialogTrigger,
  Heading,
  Icon,
  IconButton,
  Text,
} from '@vega-ui/react';
import { XIcon } from '@vega-ui/icons';

const destructive = {
  '--button-background-color': 'var(--color-red-accent-500)',
  '--button-background-color-hover': 'var(--color-red-accent-600)',
  '--button-background-color-active': 'var(--color-red-accent-700)',
} as CSSProperties;

export default function DeleteDialog() {
  return (
    <Dialog>
      <DialogTrigger asChild>
        <Button variant='secondary' appearance='ghost'>Delete project</Button>
      </DialogTrigger>
      <DialogContent aria-labelledby='delete-project-title' aria-describedby='delete-project-warning' style={{ maxWidth: 460 }}>
        <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 'var(--spacing-8)' }}>
          <Heading id='delete-project-title' as='h2' size={4} fontWeight={600}>Delete “Mobile app”?</Heading>
          <DialogClose asChild>
            <IconButton
              aria-label='Close'
              variant='secondary'
              appearance='ghost'
              style={{ flexShrink: 0, '--icon-button-size': '24px', '--icon-button-br-ratio': 0.5 } as CSSProperties}
            >
              <Icon size='xs'><XIcon strokeWidth={2.5} /></Icon>
            </IconButton>
          </DialogClose>
        </div>
        <Text size={3} asChild>
          <p style={{ marginBlock: 'var(--spacing-4) var(--spacing-8)', color: 'var(--label-secondary)' }}>The project, its 14 environments and all deployment history will be removed.</p>
        </Text>
        <Alert id='delete-project-warning' variant='warning'>
          <AlertIcon />
          <AlertMain>
            <AlertTitle>3 services still use this project</AlertTitle>
            <AlertContent>Billing API, Notifications and Search will stop receiving configuration updates.</AlertContent>
          </AlertMain>
        </Alert>
        <form method='dialog' style={{ display: 'flex', gap: 'var(--spacing-4)', justifyContent: 'flex-end', marginBlockStart: 'var(--spacing-12)' }}>
          <Button type='submit' variant='secondary' appearance='ghost'>Cancel</Button>
          <Button type='submit' style={destructive}>Delete project</Button>
        </form>
      </DialogContent>
    </Dialog>
  );
}
```

### Ошибка

#### Ошибка: сводка ошибок над формой

При отправке невалидные поля получают `aria-invalid` и связанный `HelperText`, а алерт перечисляет все ошибки со ссылками, которые переводят фокус на поле. Ошибки обновляются по мере ввода, и сводка исчезает, когда всё исправлено.

```tsx
import { FormEvent, MouseEvent, useState } from 'react';
import {
  Alert,
  AlertIcon,
  AlertMain,
  AlertTitle,
  Button,
  HelperText,
  Label,
  Text,
  TextField,
  TextFieldInput,
} from '@vega-ui/react';

type Errors = { email?: string; seats?: string };

const validate = (email: string, seats: string): Errors => {
  const errors: Errors = {};
  if (!email.includes('@')) errors.email = 'Enter an email address like name@company.com.';
  const count = Number(seats);
  if (!Number.isInteger(count) || count < 1 || count > 500) errors.seats = 'Enter a number of seats from 1 to 500.';
  return errors;
};

export default function ErrorSummary() {
  const [email, setEmail] = useState('');
  const [seats, setSeats] = useState('0');
  const [submitted, setSubmitted] = useState(false);

  const errors = submitted ? validate(email, seats) : {};
  const messages = Object.entries(errors);

  const submit = (event: FormEvent<HTMLFormElement>) => {
    event.preventDefault();
    setSubmitted(true);
  };

  const focusField = (event: MouseEvent<HTMLAnchorElement>, id: string) => {
    event.preventDefault();
    document.getElementById(id)?.focus();
  };

  return (
    <form noValidate onSubmit={submit} style={{ display: 'grid', gap: 'var(--spacing-12)', width: '100%', maxWidth: 420 }}>
      {messages.length > 0 && (
        <Alert variant='error' role='alert'>
          <AlertIcon />
          <AlertMain>
            <AlertTitle>Fix {messages.length === 1 ? 'one error' : `${messages.length} errors`} to continue</AlertTitle>
            <ul style={{ margin: 0, paddingInlineStart: 'var(--spacing-9)' }}>
              {messages.map(([field, message]) => (
                <li key={field}>
                  <Text size={2}>
                    <a href={`#order-${field}`} onClick={(event) => focusField(event, `order-${field}`)} style={{ color: 'inherit' }}>{message}</a>
                  </Text>
                </li>
              ))}
            </ul>
          </AlertMain>
        </Alert>
      )}
      <div style={{ display: 'grid', gap: 'var(--spacing-8)' }}>
        <div style={{ display: 'grid', gap: 'var(--spacing-3)' }}>
          <Label htmlFor='order-email'>Billing email</Label>
          <TextField error={Boolean(errors.email)}>
            <TextFieldInput
              id='order-email'
              type='email'
              value={email}
              onChange={(event) => setEmail(event.target.value)}
              aria-invalid={Boolean(errors.email)}
              aria-describedby={errors.email ? 'order-email-error' : undefined}
            />
          </TextField>
          {errors.email && <HelperText id='order-email-error' error>{errors.email}</HelperText>}
        </div>
        <div style={{ display: 'grid', gap: 'var(--spacing-3)' }}>
          <Label htmlFor='order-seats'>Seats</Label>
          <TextField error={Boolean(errors.seats)}>
            <TextFieldInput
              id='order-seats'
              inputMode='numeric'
              value={seats}
              onChange={(event) => setSeats(event.target.value)}
              aria-invalid={Boolean(errors.seats)}
              aria-describedby={errors.seats ? 'order-seats-error' : undefined}
            />
          </TextField>
          {errors.seats && <HelperText id='order-seats-error' error>{errors.seats}</HelperText>}
        </div>
      </div>
      <div style={{ display: 'flex', flexWrap: 'wrap', gap: 'var(--spacing-6)', alignItems: 'center' }}>
        <Button type='submit'>Continue to payment</Button>
        <Text size={2} role='status' style={{ color: 'var(--label-secondary)' }}>{submitted && messages.length === 0 ? 'Order details are valid.' : ''}</Text>
      </div>
    </form>
  );
}
```

### Пограничный случай

#### Пограничный случай: длинное неразрывное содержимое и действия

В домене и TXT-записи нет пробелов, поэтому им нужен `overflow-wrap: anywhere`, а `AlertMain` — `min-width: 0`, чтобы текст не выходил за алерт. Действия находятся в `AlertMain` и сообщают о результате через область статуса.

```tsx
import { useState } from 'react';
import { Alert, AlertContent, AlertIcon, AlertMain, AlertTitle, Button, Text } from '@vega-ui/react';

const record = 'vega-site-verification=8c1f0b6e2d9a4f7b93e5c2a1d0f4e6b8a7c3d9e1f2b4a6c8d0e2f4a6b8c0d2e4';

export default function DomainVerification() {
  const [message, setMessage] = useState('');

  const copy = async () => {
    await navigator.clipboard.writeText(record);
    setMessage('Copied to clipboard.');
  };

  const check = () => {
    setMessage('Checking DNS…');
    setTimeout(() => setMessage('Record not found yet. Try again in a few minutes.'), 1000);
  };

  return (
    <Alert variant='warning' style={{ width: '100%', maxWidth: 560 }}>
      <AlertIcon />
      <AlertMain style={{ minWidth: 0, gap: 'var(--spacing-4)' }}>
        <AlertTitle style={{ overflowWrap: 'anywhere' }}>Verify northwind-logistics-international.example.com</AlertTitle>
        <AlertContent>Add this TXT record at your DNS provider. Changes can take up to 48 hours to apply.</AlertContent>
        <Text size={1} asChild>
          <code style={{ display: 'block', fontFamily: 'var(--code-font-family)', wordBreak: 'break-all', padding: 'var(--spacing-4) var(--spacing-6)', borderRadius: 'var(--radius-2)', background: 'var(--fills-tertiary)', color: 'var(--label-primary)' }}>{record}</code>
        </Text>
        <div style={{ display: 'flex', flexWrap: 'wrap', gap: 'var(--spacing-4)', alignItems: 'center', marginBlockStart: 'var(--spacing-2)' }}>
          <Button size='sm' onClick={copy}>Copy record</Button>
          <Button size='sm' variant='secondary' appearance='ghost' onClick={check}>Check again</Button>
          <Text size={2} role='status' style={{ color: 'var(--label-secondary)' }}>{message}</Text>
        </div>
      </AlertMain>
    </Alert>
  );
}
```

## Анатомия

### Обзор

`Alert` — [составной компонент](https://vega-ui.dev/ru/docs/glossary/#составной-компонент-compound-component): корень, иконка, заголовок и текст работают вместе как единый паттерн сообщения с общим контекстом семантического варианта.

### Обязательные части

#### `Alert`

Обязательна. Отвечает за семантический вариант и внешний вид.

#### `AlertMain`

Обязательна в большинстве продуктовых алертов. Оборачивает иерархию сообщения.

### Необязательные части

#### `AlertIcon`

Необязательная поверхность для семантической иконки.

#### `AlertTitle`

Необязательный заголовок для более сильного акцента.

#### `AlertContent`

Необязательный поясняющий текст сообщения.

### Порядок композиции

1. `Alert`
2. `AlertIcon`
3. `AlertMain`
4. `AlertTitle`
5. `AlertContent`

### Корректные варианты композиции

```tsx
import { Alert, AlertContent, AlertIcon, AlertMain, AlertTitle } from '@vega-ui/react';

export default function Variants() {
  return (
    <div style={{ display: 'grid', gap: 'var(--spacing-6)', width: '100%', maxWidth: 560 }}>
      <Alert variant='info'>
        <AlertIcon />
        <AlertMain>
          <AlertTitle>Scheduled maintenance</AlertTitle>
          <AlertContent>The dashboard will be read-only on Sunday from 02:00 to 03:00 UTC.</AlertContent>
        </AlertMain>
      </Alert>
      <Alert variant='success'>
        <AlertIcon />
        <AlertMain>
          <AlertTitle>Domain verified</AlertTitle>
          <AlertContent>Emails from billing@northwind.io will now pass SPF and DKIM checks.</AlertContent>
        </AlertMain>
      </Alert>
      <Alert variant='warning'>
        <AlertIcon />
        <AlertMain>
          <AlertTitle>Storage almost full</AlertTitle>
          <AlertContent>You have used 46.2 GB of 50 GB. Uploads stop when the limit is reached.</AlertContent>
        </AlertMain>
      </Alert>
      <Alert variant='error' appearance='surface'>
        <AlertIcon />
        <AlertMain>
          <AlertTitle>Card declined</AlertTitle>
          <AlertContent>The payment for invoice INV-2048 was declined by the bank. Update the card to keep your plan.</AlertContent>
        </AlertMain>
      </Alert>
    </div>
  );
}
```

### Некорректные варианты композиции

#### Alert в роли каркаса всей страницы

Алерты — это сообщения обратной связи, а не замена структуре страницы.

#### Длинный сценарий с множеством действий внутри алерта

Когда содержимое превращается в полноценную задачу, обычно лучше подходит другой паттерн.
