# Icon

## Кратко

`Icon` — общая SVG-обёртка для иконок VegaUI и собственного SVG-содержимого. Она нормализует размер через токены и рендерит через `Slot`, поэтому дочерний SVG напрямую получает объединённые пропсы.

## Импорт

```tsx
import { Icon, type IconProps, type IconSize } from '@vega-ui/react';
```

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

- `IconProps`
- `IconSize`

## Базовое использование

```tsx
import { Icon } from '@vega-ui/react';
import { CalendarIcon, MessageSquareIcon, PaperclipIcon } from '@vega-ui/icons';

export default function BasicUsage() {
  return (
    <ul style={{ display: 'flex', flexWrap: 'wrap', gap: 'var(--spacing-4) var(--spacing-10)', margin: 0, padding: 0, listStyle: 'none', color: 'var(--label-secondary)' }}>
      <li style={{ display: 'flex', alignItems: 'center', gap: 'var(--spacing-3)' }}>
        <Icon size='sm'><CalendarIcon /></Icon>
        Due May 14
      </li>
      <li style={{ display: 'flex', alignItems: 'center', gap: 'var(--spacing-3)' }}>
        <Icon size='sm'><MessageSquareIcon /></Icon>
        12 comments
      </li>
      <li style={{ display: 'flex', alignItems: 'center', gap: 'var(--spacing-3)' }}>
        <Icon size='sm'><PaperclipIcon /></Icon>
        3 files
      </li>
    </ul>
  );
}
```

## Варианты

- Размер: `4xs`, `3xs`, `2xs`, `xs`, `sm`, `md`, `lg`, `xl`
- Способ задать размер: токен или явные `width` / `height`
- Содержимое: иконка VegaUI, собственный SVG или SVG-компонент сторонней библиотеки

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

- Явные `width` или `height` обходят data-атрибут размера из токена.
- `Icon` по умолчанию ставит `aria-hidden='true'` — это верно для декоративных иконок, но не для самостоятельной смысловой графики.
- `stroke='currentColor'` означает, что цвет иконки берётся из окружающего текста или элемента управления.

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

- Передача состояния только цветом иконки.
- Размер иконки расходится с плотностью окружающего элемента управления.
- Предположение, что каждую иконку должны объявлять вспомогательные технологии.

## Примеры

### Базовый

#### Базовый: иконки рядом с текстом

`Icon` добавляет дочернему SVG `aria-hidden='true'` и `stroke='currentColor'`, поэтому глиф рядом с текстом декоративен и берёт цвет текста родителя.

```tsx
import { Icon, Text } from '@vega-ui/react';
import { CircleCheckIcon, ShieldIcon, TriangleAlertIcon } from '@vega-ui/icons';

export default function InfoRow() {
  return (
    <ul style={{ display: 'flex', flexDirection: 'column', gap: 'var(--spacing-4)', margin: 0, padding: 0, listStyle: 'none', width: '100%' }}>
      <li style={{ display: 'flex', alignItems: 'flex-start', gap: 'var(--spacing-4)' }}>
        <Icon size='sm' style={{ flexShrink: 0, marginBlockStart: 'var(--spacing-3)', color: 'var(--color-green-accent-500)' }}><CircleCheckIcon /></Icon>
        <Text size={3}>SSO is enabled for this workspace</Text>
      </li>
      <li style={{ display: 'flex', alignItems: 'flex-start', gap: 'var(--spacing-4)' }}>
        <Icon size='sm' style={{ flexShrink: 0, marginBlockStart: 'var(--spacing-3)', color: 'var(--color-primary-500)' }}><ShieldIcon /></Icon>
        <Text size={3}>Two-factor authentication is required for admins</Text>
      </li>
      <li style={{ display: 'flex', alignItems: 'flex-start', gap: 'var(--spacing-4)' }}>
        <Icon size='sm' style={{ flexShrink: 0, marginBlockStart: 'var(--spacing-3)', color: 'var(--color-error)' }}><TriangleAlertIcon /></Icon>
        <Text size={3}>2 members have not verified their email</Text>
      </li>
    </ul>
  );
}
```

#### Базовый: шкала размеров

`size` соответствует фиксированным размерам от `4xs` (8px) до `xl` (28px). Выбирайте размер под текст или контрол рядом с иконкой.

```tsx
import { Icon, type IconSize } from '@vega-ui/react';
import { StarIcon } from '@vega-ui/icons';

const sizes: IconSize[] = ['4xs', '3xs', '2xs', 'xs', 'sm', 'md', 'lg', 'xl'];

export default function Sizes() {
  return (
    <div style={{ display: 'flex', alignItems: 'flex-end', gap: 'var(--spacing-8)' }}>
      {sizes.map((size) => (
        <figure key={size} style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 'var(--spacing-3)', margin: 0 }}>
          <Icon size={size}><StarIcon /></Icon>
          <figcaption style={{ color: 'var(--label-secondary)', fontSize: 'var(--font-size-1)' }}>{size}</figcaption>
        </figure>
      ))}
    </div>
  );
}
```

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

#### Контролируемый/с состоянием: шеврон раскрытия

Шеврон поворачивается через CSS, когда раздел раскрывается; само состояние сообщает `aria-expanded` на кнопке, а не иконка.

```tsx
import { Button, Icon, Text } from '@vega-ui/react';
import { ChevronDownIcon } from '@vega-ui/icons';
import { useState } from 'react';
import './examples.css';

export default function DisclosureChevron() {
  const [expanded, setExpanded] = useState(false);

  return (
    <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'flex-start', gap: 'var(--spacing-4)', width: '100%' }}>
      <Button
        type='button'
        variant='secondary'
        appearance='ghost'
        aria-expanded={expanded}
        aria-controls='shipping-details'
        onClick={() => setExpanded(!expanded)}
      >
        Shipping details
        <Icon size='sm' className='disclosureChevron' style={{ transform: expanded ? 'rotate(180deg)' : 'none' }}>
          <ChevronDownIcon />
        </Icon>
      </Button>
      <div id='shipping-details' hidden={!expanded}>
        <Text size={3} style={{ color: 'var(--label-secondary)' }}>Ships in 2–3 business days from Berlin. Free returns within 30 days.</Text>
      </div>
    </div>
  );
}
```

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

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

`TextField` — flex-контейнер, поэтому `Icon` перед `TextFieldInput` становится украшением поля. У поля остаётся видимый `Label`; иконка только декорирует.

```tsx
import { Icon, Label, TextField, TextFieldInput } from '@vega-ui/react';
import { AtSignIcon } from '@vega-ui/icons';
import { FormEvent, useState } from 'react';

export default function InputAdornment() {
  const [handle, setHandle] = useState('');

  const handleSubmit = (event: FormEvent<HTMLFormElement>) => {
    event.preventDefault();
    setHandle(String(new FormData(event.currentTarget).get('handle')));
  };

  return (
    <form onSubmit={handleSubmit} style={{ display: 'flex', flexDirection: 'column', gap: 'var(--spacing-3)', width: '100%' }}>
      <Label htmlFor='profile-handle'>Username</Label>
      <TextField style={{ paddingInlineStart: 'var(--spacing-8)', gap: 'var(--spacing-3)' }}>
        <Icon size='sm' style={{ flexShrink: 0, color: 'var(--label-secondary)' }}><AtSignIcon /></Icon>
        <TextFieldInput id='profile-handle' name='handle' placeholder='jane.doe' pattern='[a-z0-9._]+' required style={{ paddingInlineStart: 0 }} />
      </TextField>
      <output style={{ color: 'var(--label-secondary)', overflowWrap: 'anywhere' }}>
        {handle === '' ? 'Press Enter to save' : `Profile URL: example.com/@${handle}`}
      </output>
    </form>
  );
}
```

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

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

Иконки в начале и в конце встают в flex-зазор кнопки. Внутри `Button` иконка не масштабируется сама, поэтому задайте `size` под размер кнопки.

```tsx
import { Button, Icon } from '@vega-ui/react';
import { ChevronDownIcon, DownloadIcon, PlusIcon } from '@vega-ui/icons';

export default function IconInsideAButton() {
  return (
    <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 'var(--spacing-4)' }}>
      <Button type='button'>
        <Icon size='sm'><PlusIcon /></Icon>
        Add member
      </Button>
      <Button type='button' variant='secondary' appearance='ghost'>
        Export
        <Icon size='sm'><ChevronDownIcon /></Icon>
      </Button>
      <Button type='button' size='sm' variant='secondary' appearance='ghost'>
        <Icon size='xs'><DownloadIcon /></Icon>
        Invoice.pdf
      </Button>
    </div>
  );
}
```

### Ошибка

#### Ошибка: иконка в сообщении валидации

Поле проверяется при потере фокуса. Смысл несёт текст ошибки, а не иконка; иконка наследует цвет ошибки от `HelperText`, а сообщение связано через `aria-describedby`.

```tsx
import { HelperText, Icon, Label, TextField, TextFieldInput } from '@vega-ui/react';
import { CircleAlertIcon } from '@vega-ui/icons';
import { useState } from 'react';

export default function ErrorMessageIcon() {
  const [error, setError] = useState('');

  const validate = (input: HTMLInputElement) => {
    if (input.validity.valid) {
      setError('');
      return;
    }

    setError('Enter an email like name@company.com');
  };

  return (
    <div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--spacing-3)', width: '100%' }}>
      <Label htmlFor='billing-email'>Billing email</Label>
      <TextField error={error !== ''}>
        <TextFieldInput
          id='billing-email'
          type='email'
          defaultValue='finance.acme.com'
          required
          aria-invalid={error !== ''}
          aria-describedby={error === '' ? undefined : 'billing-email-error'}
          onBlur={(event) => validate(event.currentTarget)}
        />
      </TextField>
      {error !== '' && (
        <HelperText id='billing-email-error' error style={{ display: 'flex', alignItems: 'center', gap: 'var(--spacing-3)' }}>
          <Icon size='xs' style={{ flexShrink: 0 }}><CircleAlertIcon /></Icon>
          {error}
        </HelperText>
      )}
    </div>
  );
}
```

### Отключённый

#### Отключённый: иконки в отключённых контролах

Глиф использует `currentColor`, поэтому берёт цвет отключённого текста кнопки без дополнительных стилей. То же работает для приглушённого поясняющего текста.

```tsx
import { Button, Icon, IconButton } from '@vega-ui/react';
import { LockIcon, Trash2Icon, UploadIcon } from '@vega-ui/icons';

export default function MutedControlIcon() {
  return (
    <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'flex-start', gap: 'var(--spacing-6)', width: '100%' }}>
      <div style={{ display: 'flex', gap: 'var(--spacing-4)' }}>
        <Button type='button' disabled>
          <Icon size='sm'><UploadIcon /></Icon>
          Upload logo
        </Button>
        <IconButton type='button' variant='secondary' appearance='ghost' aria-label='Delete logo' disabled>
          <Icon><Trash2Icon /></Icon>
        </IconButton>
      </div>
      <p style={{ display: 'flex', alignItems: 'center', gap: 'var(--spacing-3)', margin: 0, color: 'var(--disable-text-color)', fontSize: 'var(--font-size-2)' }}>
        <Icon size='xs' style={{ flexShrink: 0 }}><LockIcon /></Icon>
        Branding is managed by your organization admin
      </p>
    </div>
  );
}
```

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

#### Пограничный случай: иконка как единственный сигнал статуса

В колонке статуса нет видимого текста, а иконка остаётся `aria-hidden`. `VisuallyHidden` сообщает статус скринридеру словами.

```tsx
import { Icon, VisuallyHidden } from '@vega-ui/react';
import { CircleCheckIcon, CircleXIcon } from '@vega-ui/icons';

const checks = [
  { name: 'Unit tests', passed: true },
  { name: 'Lint', passed: true },
  { name: 'Visual regression', passed: false },
];

export default function StandaloneMeaningfulIcon() {
  return (
    <table style={{ width: '100%', borderCollapse: 'collapse' }}>
      <thead>
        <tr>
          <th scope='col' style={{ paddingBlock: 'var(--spacing-4)', borderBlockEnd: '1px solid var(--separator-opaque)', textAlign: 'start' }}>Check</th>
          <th scope='col' style={{ paddingBlock: 'var(--spacing-4)', borderBlockEnd: '1px solid var(--separator-opaque)', textAlign: 'end' }}>Status</th>
        </tr>
      </thead>
      <tbody>
        {checks.map((check) => (
          <tr key={check.name}>
            <td style={{ paddingBlock: 'var(--spacing-4)', borderBlockEnd: '1px solid var(--separator-opaque)' }}>{check.name}</td>
            <td style={{ paddingBlock: 'var(--spacing-4)', borderBlockEnd: '1px solid var(--separator-opaque)', textAlign: 'end' }}>
              <Icon
                size='sm'
                style={{ color: check.passed ? 'var(--color-green-accent-500)' : 'var(--color-error)' }}
              >
                {check.passed ? <CircleCheckIcon /> : <CircleXIcon />}
              </Icon>
              <VisuallyHidden asChild>
                <span>{check.passed ? 'Passed' : 'Failed'}</span>
              </VisuallyHidden>
            </td>
          </tr>
        ))}
      </tbody>
    </table>
  );
}
```

#### Пограничный случай: направленные иконки в раскладке справа налево

Стрелки указывают в сторону чтения, поэтому в `dir='rtl'` их нужно отражать. Отражайте только направленные глифы; галочка или часы остаются как есть.

```tsx
import { Button, Icon } from '@vega-ui/react';
import { ArrowRightIcon } from '@vega-ui/icons';

export default function RtlDirectionalIcon() {
  return (
    <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'flex-start', gap: 'var(--spacing-4)' }}>
      <div dir='ltr'>
        <Button type='button'>
          Continue
          <Icon size='sm'><ArrowRightIcon /></Icon>
        </Button>
      </div>
      <div dir='rtl' lang='ar'>
        <Button type='button'>
          متابعة
          <Icon size='sm' style={{ transform: 'scaleX(-1)' }}><ArrowRightIcon /></Icon>
        </Button>
      </div>
    </div>
  );
}
```

#### Пограничный случай: размер вне шкалы

Для иконки размером с иллюстрацию задайте `--icon-size` напрямую. Пропы `width` и `height` сейчас не передаются в SVG, поэтому не полагайтесь на них.

```tsx
import type { CSSProperties } from 'react';
import { Heading, Icon, Text } from '@vega-ui/react';
import { InboxIcon } from '@vega-ui/icons';

export default function CustomIconSize() {
  return (
    <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 'var(--spacing-8)', width: '100%', textAlign: 'center' }}>
      <Icon strokeWidth={1.25} style={{ '--icon-size': '64px', color: 'var(--label-tertiary)' } as CSSProperties}>
        <InboxIcon />
      </Icon>
      <div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--spacing-2)' }}>
        <Heading as='h3' size={4} fontWeight={600}>Inbox zero</Heading>
        <Text size={3} style={{ color: 'var(--label-secondary)', textWrap: 'balance' }}>New requests from your team show up here.</Text>
      </div>
    </div>
  );
}
```

## Анатомия

### Обзор

`Icon` — примитив из одной части, который передаёт размер и SVG-пропсы дочернему элементу через `Slot`.

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

#### `Icon`

Обязательна. Владеет общим контрактом размерных токенов и пробросом SVG-пропсов.

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

Экспортируемых дочерних частей нет.

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

1. `Icon`
2. один дочерний SVG

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

```tsx
import { Icon } from '@vega-ui/react';
import { CalendarIcon, MessageSquareIcon, PaperclipIcon } from '@vega-ui/icons';

export default function BasicUsage() {
  return (
    <ul style={{ display: 'flex', flexWrap: 'wrap', gap: 'var(--spacing-4) var(--spacing-10)', margin: 0, padding: 0, listStyle: 'none', color: 'var(--label-secondary)' }}>
      <li style={{ display: 'flex', alignItems: 'center', gap: 'var(--spacing-3)' }}>
        <Icon size='sm'><CalendarIcon /></Icon>
        Due May 14
      </li>
      <li style={{ display: 'flex', alignItems: 'center', gap: 'var(--spacing-3)' }}>
        <Icon size='sm'><MessageSquareIcon /></Icon>
        12 comments
      </li>
      <li style={{ display: 'flex', alignItems: 'center', gap: 'var(--spacing-3)' }}>
        <Icon size='sm'><PaperclipIcon /></Icon>
        3 files
      </li>
    </ul>
  );
}
```

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

#### Дочернее содержимое, не являющееся SVG

Обёртка предназначена для проброса SVG-пропсов и размеров.

#### Самостоятельная смысловая иконка без подписи или контекста

Поведение `aria-hidden` по умолчанию может сделать её смысл недоступным.
