# Дизайн-система Microsoft Fluent 2

## Предварительный просмотр интерфейса дизайн-система Microsoft Fluent 2

Переключайтесь между разделами «Краткое описание» и «Люди», меняйте веб-тему и проверяйте локальное добавление с ошибками, дубликатами и успешным результатом. Приглашение не отправляется.

## Microsoft Fluent 2 Справочник по дизайн-системе

Веб-справочники Fluent UI React v9. Глобальные числовые значения взяты из связанных источников токенов React. Приведённые ниже значения цветов получены из сопоставления светлой темы с алиасами brandWeb, а не из устаревших комментариев с шестнадцатеричными значениями в alias/lightColor.ts. Для тёмных цветов и тем Teams используются отдельные сопоставления. В таблице типографики для веб-сайта сейчас указано значение Subtitle1 20/26; в исходном файле React fonts.ts используется 20/28 — именно это значение применено здесь. Источники проверены 3 октября 2026 года; сверяйте значения с версией, указанной в вашем lockfile.

### Нейтральные роли светлой темы

| Название / правило | Значение / конфигурация | Применение |
| --- | --- | --- |
| colorNeutralForeground1 | #242424 | Основной текст документа. |
| colorNeutralForeground2 | #424242 | Вторичные подписи. |
| colorNeutralForeground3 | #616161 | Метаданные; всё равно проверьте итоговую контрастность. |
| colorNeutralBackground1 | #ffffff | Основная поверхность. |
| colorNeutralBackground2 | #fafafa | Ненавязчивая вспомогательная поверхность. |
| colorNeutralBackground3 | #f5f5f5 | Фон рабочей области. |
| colorNeutralStroke1 | #d1d1d1 | Обычная граница компонента. |
| colorNeutralStroke2 | #e0e0e0 | Ненавязчивая разделительная линия. |
| colorNeutralStrokeAccessible | #616161 | Граница элемента управления с повышенной контрастностью. |
| colorNeutralForegroundOnBrand | #ffffff | Текст на поддерживаемых фирменных фонах. |
| colorStrokeFocus1 / colorStrokeFocus2 | #ffffff / #000000 | Два цвета фокуса; сохраняйте контрастность по краю компонента. |
### Веб-роли бренда, полученные из brandWeb

| Название / правило | Значение / конфигурация | Применение |
| --- | --- | --- |
| colorBrandBackground | #0f6cbd · brandWeb[80] | Основное действие в webLightTheme. |
| colorBrandBackgroundHover | #115ea3 · brandWeb[70] | Состояние при наведении для этого действия. |
| colorBrandBackgroundPressed | #0c3b5e · brandWeb[40] | Состояние нажатия; не является универсальным статусом выбора. |
| colorBrandBackground2 | #ebf3fc · brandWeb[160] | Ненавязчивый фирменный фон. |
| colorBrandForegroundLink | #115ea3 · brandWeb[70] | Цвет ссылки в светлой веб-теме. |
### Горизонтальные и вертикальные интервалы

| Название / правило | Значение / конфигурация | Применение |
| --- | --- | --- |
| spacingHorizontalNone / spacingVerticalNone | 0 | Выбирайте ось в соответствии с направлением компоновки; общий шаг интервалов. |
| spacingHorizontalXXS / spacingVerticalXXS | 2px | Выбирайте ось в соответствии с направлением компоновки; общий шаг интервалов. |
| spacingHorizontalXS / spacingVerticalXS | 4px | Выбирайте ось в соответствии с направлением компоновки; общий шаг интервалов. |
| spacingHorizontalSNudge / spacingVerticalSNudge | 6px | Выбирайте ось в соответствии с направлением компоновки; небольшая поправка для выравнивания значка. |
| spacingHorizontalS / spacingVerticalS | 8px | Выбирайте ось в соответствии с направлением компоновки; общий шаг интервалов. |
| spacingHorizontalMNudge / spacingVerticalMNudge | 10px | Выбирайте ось в соответствии с направлением компоновки; небольшая поправка для выравнивания значка. |
| spacingHorizontalM / spacingVerticalM | 12px | Выбирайте ось в соответствии с направлением компоновки; общий шаг интервалов. |
| spacingHorizontalL / spacingVerticalL | 16px | Выбирайте ось в соответствии с направлением компоновки; общий шаг интервалов. |
| spacingHorizontalXL / spacingVerticalXL | 20px | Выбирайте ось в соответствии с направлением компоновки; общий шаг интервалов. |
| spacingHorizontalXXL / spacingVerticalXXL | 24px | Выбирайте ось в соответствии с направлением компоновки; общий шаг интервалов. |
| spacingHorizontalXXXL / spacingVerticalXXXL | 32px | Выбирайте ось в соответствии с направлением компоновки; общий шаг интервалов. |
### Комбинации веб-типографики React

| Название / правило | Значение / конфигурация | Применение |
| --- | --- | --- |
| Caption2 · fontSizeBase100 | 10px / 14px / 400 | Размер / высота строки / насыщенность. Используйте вместе с lineHeightBase100. |
| Caption1 · fontSizeBase200 | 12px / 16px / 400 | Размер / межстрочный интервал / насыщенность. Используйте вместе с lineHeightBase200. |
| Body1 · fontSizeBase300 | 14px / 20px / 400 | Размер / межстрочный интервал / насыщенность. Используйте вместе с lineHeightBase300. |
| Body2 / Subtitle2 · fontSizeBase400 | 16px / 22px / 400 / 600 | Размер / межстрочный интервал / насыщенность. Используйте вместе с lineHeightBase400. |
| Subtitle1 · fontSizeBase500 | 20px / 28px / 600 | Размер / межстрочный интервал / насыщенность. Используйте вместе с lineHeightBase500. |
| Title3 · fontSizeBase600 | 24px / 32px / 600 | Размер / межстрочный интервал / насыщенность. Используйте вместе с lineHeightBase600. |
| Title2 · fontSizeHero700 | 28px / 36px / 600 | Размер / межстрочный интервал / насыщенность. Используйте вместе с lineHeightHero700. |
| Title1 · fontSizeHero800 | 32px / 40px / 600 | Размер / межстрочный интервал / насыщенность. Используйте вместе с lineHeightHero800. |
| LargeTitle · fontSizeHero900 | 40px / 52px / 600 | Размер / межстрочный интервал / насыщенность. Используйте вместе с lineHeightHero900. |
| Display · fontSizeHero1000 | 68px / 92px / 600 | Размер / межстрочный интервал / насыщенность. Используйте вместе с lineHeightHero1000. |
### Семейство шрифтов и насыщенность

| Название / правило | Значение / конфигурация | Применение |
| --- | --- | --- |
| fontFamilyBase | 'Segoe UI', 'Segoe UI Web (West European)', -apple-system, BlinkMacSystemFont, Roboto, 'Helvetica Neue', sans-serif | Стандартный веб-стек провайдера; не называйте шрифт остальной части сайта Fluent. |
| fontFamilyMonospace | Consolas, 'Courier New', Courier, monospace | Код и технические идентификаторы. |
| fontWeightRegular / Medium / Semibold / Bold | 400 / 500 / 600 / 700 | Используйте насыщенность шрифта, соответствующую выбранной типографической шкале. |
### Токены формы

| Название / правило | Значение / конфигурация | Применение |
| --- | --- | --- |
| borderRadiusNone | 0 | Сначала используйте стандартный вариант компонента: этот токен доступен, но не обязателен для каждой поверхности. |
| borderRadiusSmall | 2px | Сначала используйте стандартный вариант компонента: этот токен доступен, но не обязателен для каждой поверхности. |
| borderRadiusMedium | 4px | Сначала используйте стандартный вариант компонента: этот токен доступен, но не обязателен для каждой поверхности. |
| borderRadiusLarge | 6px | Сначала используйте стандартный вариант компонента: этот токен доступен, но не обязателен для каждой поверхности. |
| borderRadiusXLarge | 8px | Сначала используйте стандартный вариант компонента: этот токен доступен, но не обязателен для каждой поверхности. |
| borderRadius2XLarge | 12px | Сначала используйте стандартный вариант компонента: этот токен доступен, но не обязателен для каждой поверхности. |
| borderRadius3XLarge | 16px | Сначала используйте стандартный вариант компонента: этот токен доступен, но не обязателен для каждой поверхности. |
| borderRadius4XLarge | 24px | Сначала используйте стандартный вариант компонента: этот токен доступен, но не обязателен для каждой поверхности. |
| borderRadius5XLarge | 32px | Сначала используйте стандартный вариант компонента: этот токен доступен, но не обязателен для каждой поверхности. |
| borderRadius6XLarge | 40px | Сначала используйте стандартный вариант компонента: этот токен доступен, но не обязателен для каждой поверхности. |
| borderRadiusCircular | 10000px | Сначала используйте стандартный вариант компонента: этот токен доступен, но не обязателен для каждой поверхности. |
### Шкала длительности анимации

| Название / правило | Значение / конфигурация | Применение |
| --- | --- | --- |
| durationUltraFast | 50ms | Доступный токен среды выполнения; в примере для наведения используется Fast, а при включённом режиме уменьшения анимации переходы отключаются. |
| durationFaster | 100ms | Доступный токен среды выполнения; в примере для наведения используется Fast, а при включённом режиме уменьшения анимации переходы отключаются. |
| durationFast | 150ms | Доступный токен среды выполнения; в примере для наведения используется Fast, а при включённом режиме уменьшения анимации переходы отключаются. |
| durationNormal | 200ms | Доступный токен среды выполнения; в примере для наведения используется Fast, а при включённом режиме уменьшения анимации переходы отключаются. |
| durationGentle | 250ms | Доступный токен среды выполнения; в примере для наведения используется Fast, а при включённом режиме уменьшения анимации переходы отключаются. |
| durationSlow | 300ms | Доступный токен среды выполнения; в примере для наведения используется Fast, а при включённом режиме уменьшения анимации переходы отключаются. |
| durationSlower | 400ms | Доступный токен среды выполнения; в примере для наведения используется Fast, а при включённом режиме уменьшения анимации переходы отключаются. |
| durationUltraSlow | 500ms | Доступный токен среды выполнения; в примере для наведения используется Fast, а при включённом режиме уменьшения анимации переходы отключаются. |
### Смена темы — это изменение семантики, а не инверсия цветов

| Название / правило | Значение / конфигурация | Применение |
| --- | --- | --- |
| colorNeutralBackground1 | #292929 | В тёмной теме используется основная поверхность вместо светлого #ffffff. |
| colorNeutralForeground1 | #ffffff | В тёмной теме используется основной текст вместо светлого #242424. |
| colorNeutralForeground2 | #d6d6d6 | Вторичный текст в тёмной теме. |
| colorBrandBackground | brandWeb[70] = #115ea3 | Основная заливка в тёмной теме; в светлой теме используется brandWeb[80]. |
| colorBrandForegroundLink | brandWeb[100] = #479ef5 | Роль ссылки в тёмной теме; она отличается от заливки основной кнопки. |
## Полное руководство по дизайн-системе Microsoft Fluent 2

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

Подробные рекомендации, отправная точка для реализации и ссылки на источники.

## Дизайн-система Microsoft Fluent 2 и реализация в React v9

### Явно выберите веб-реализацию

> Fluent 2 — это кроссплатформенный язык дизайна. В этом руководстве рассматривается более узкий и практически применимый вариант: библиотека @fluentui/react-components, конвергентная библиотека React v9. Корневой компонент — FluentProvider; в этом примере используются webLightTheme или webDarkTheme. Тема Teams — отдельная конфигурация с собственным брендингом: привычный фиолетовый цвет Teams не является стандартным синим диапазоном веб-темы. Ресурсы для Windows, iOS и Android также не взаимозаменяемы с пакетами React.

Пример ниже целиком отображает описание проекта и локальный список пользователей с доступом. Для создания иерархии он сочетает Title3 и Body1, для двух представлений — TabList и Tab, а для принятия решения о доступе — Dialog с Field, Input и Select. Код не заявляет, что действительно отправляет приглашение человеку. Благодаря этому поведение компонентов можно продемонстрировать, не выдумывая сервис для совместного доступа.

Прочитать вспомогательную спецификацию

### Что именно предоставляет провайдер

FluentProvider добавляет переменные темы в своё поддерево. Пользовательская поверхность со стилем на основе токена colorNeutralBackground2 будет соответствовать выбранной теме, а поверхность с жёстко заданным белым цветом — нет. Размещайте провайдер выше компонентов, которым нужна тема. Библиотека стилизует компоненты с помощью среды выполнения для стилей; в примере не предлагается импортировать несуществующий файл fluent.css.

Прочитать вспомогательную спецификацию

## Семантические цвета дизайн-системы Microsoft Fluent 2 и фактический веб-диапазон бренда

### Сопоставляйте псевдонимы с выбранным брендом

В светлой теме псевдоним colorBrandBackground указывает на brand[80]. В веб-теме используется brandWeb, где шаг 80 — это #0f6cbd. Поэтому в этом примере итоговая основная заливка — #0f6cbd. В файле псевдонимов остались старые иллюстративные комментарии с шестнадцатеричными значениями; если принять их за актуальную веб-палитру, результат будет неверным. Значение определяется связью между исходными токенами, а не комментарием.

Для описания используйте colorNeutralForeground1, для вспомогательных подписей — colorNeutralForeground2, а для окружающей рабочей области — colorNeutralBackground3. Для основного действия подойдут colorBrandBackground и colorNeutralForegroundOnBrand. Названия ролей остаются неизменными при смене темы. Разрешение пользователя указывается текстом; синяя рамка или индикатор синей вкладки обозначают выбор, а не наличие доступа.

Прочитать вспомогательную спецификацию

### В тёмной теме меняется соответствие

В тёмной теме основная поверхность становится #292929, а основной текст — #ffffff. Для основной заливки бренда используется шаг 70, а для текста ссылки — шаг 100. Поэтому в предпросмотре роли токенов меняются согласованно, а не просто инвертируется фон страницы. После переключения сравните диалог доступа, выбранную вкладку и приглушённые метаданные: все они зависят от одной границы темы.

Прочитать вспомогательную спецификацию

## Типографика дизайн-системы Microsoft Fluent 2: сопоставление текстовых ролей с токенами среды выполнения

### Используйте парные значения размера и межстрочного интервала

Body1 — базовый стиль для чтения: размер 14px и межстрочный интервал 20px. Caption1 — 12/16, Subtitle2 — 16/22 с полужирным начертанием, а Title3 — 24/32 с полужирным начертанием. Эти пары помогают выстроить чёткую иерархию заголовков, подписей и основного текста без произвольных промежуточных размеров. В таблице выше для справки приведены и более крупные значения, но компактной рабочей области не нужен заголовок размером с титульный.

В источнике токенов React и на сайте с документацией по дизайну сейчас есть расхождение в одном полезном значении: в исходниках среды выполнения fontSizeBase500 / lineHeightBase500 соответствует 20/28, а в веб-таблице дизайна для Subtitle1 указано 20/26. В этом примере используется пара из среды выполнения. При таких различиях проверяйте версию и реализацию, а не объединяйте значения без пояснений, выбирая те, что лучше подходят макету.

Прочитать вспомогательную спецификацию

### Текстовые пресеты и стек шрифтов

Используйте семантическую разметку HTML вместе с визуальным пресетом: Title3 as="h1" задаёт заголовок, тогда как одно лишь оформление не создаёт структуру документа. В начале базового стека стоит Segoe UI, за которым идут системные запасные шрифты. Не добавляйте проприетарный шрифт только ради совпадения со снимком экрана. В образце используется документированный стек, поэтому точный вид глифов зависит от шрифтов, установленных на устройстве просмотра.

Для пользовательского текста typographyStyles.body1 объединяет семейство, размер, насыщенность и межстрочный интервал. Предпочитайте этот набор копированию одного только fontSizeBase300. В списке доступа адреса электронной почты удобно читать и переносить на следующую строку; метка разрешения выделена полужирным, но не конкурирует с заголовком страницы.

Прочитать вспомогательную спецификацию

## Интервалы, форма и плотность в рабочей области дизайн-системы Microsoft Fluent 2

### Названия интервалов React отличаются от названий в инструменте проектирования

В общих рекомендациях Fluent по компоновке используются глобальные названия размеров и шаг в четыре единицы, включая значения 2, 6 и 10 для корректировки выравнивания. В React доступны псевдонимы для горизонтальных и вертикальных интервалов: spacingHorizontalS — 8px, M — 12px, L — 16px и XXL — 24px; для вертикальных интервалов предусмотрены соответствующие названия. Не преобразуйте size120 в spacingHorizontal120: такого идентификатора React не существует.

В этой композиции используются внутренние отступы панели 24px, промежуток 16px между полями формы и интервал 12px между строками списка доступа. Точная ширина страницы и расположение в два столбца — решения приложения, а не официальные контрольные точки Fluent. На узком экране дополнительный контекст команды перемещается под описание проекта, а обе метки вкладок остаются видимыми.

Прочитать вспомогательную спецификацию

### Форма отражает размер компонента

Среди доступных токенов радиуса есть Medium — 4px и XLarge — 8px, но их наличие не означает, что всем элементам управления следует задавать одинаковый радиус. В образце HTML форма элемента управления 4px, а поверхности документа — 8px; это явные решения для примера. У стандартных компонентов следует сохранять поддерживаемую по умолчанию структуру, если только выбранные требования к дизайну не оправдывают переопределение. Не обращайтесь к сгенерированным именам классов, чтобы принудительно изменить форму.

Прочитать вспомогательную спецификацию

## TabList в дизайн-системе Microsoft Fluent 2: выбор, панели и поведение клавиатуры

### Выбранное значение — состояние приложения

В примере приложение хранит вкладку в состоянии React и передаёт её в selectedValue. Обработчик onTabSelect получает data.value и обновляет это состояние. Для каждой Tab задано стабильное значение и ссылка на панель через aria-controls. У каждой панели есть role="tabpanel", aria-labelledby и условие скрытия. Компонент не предоставляет описание проекта или список доступа — их отображение задано явно.

В API переход к фокусу и выбор разделены: по умолчанию selectTabOnFocus имеет значение false. В самостоятельном образце клавиши Left/Right и Home/End перемещают фокус, а Enter или Space активирует вкладку в фокусе. Такое поведение с ручной активацией позволяет просмотреть варианты, не переключая содержимое неожиданно. Используйте короткие подписи вкладок: согласно официальным рекомендациям по компоновке, горизонтальные вкладки автоматически не переносятся и не прокручиваются.

Прочитать вспомогательную спецификацию

### Если категорий становится больше, используйте другой шаблон

Brief и People — два тесно связанных представления одной рабочей области, поэтому здесь уместны вкладки. Список всех проектов организации — это навигация, а не ещё один ряд вкладок. Если на узком экране категории перестают помещаться, продумайте переполнение или другой способ выбора; уменьшать подписи до нечитаемого размера — не стратегия адаптивной вёрстки.

Прочитать вспомогательную спецификацию

## Field в дизайн-системе Microsoft Fluent 2: доступные подписи и содержательная валидация

### Field — это композиция, а не просто оформленный ввод

Field объединяет слот для подписи, дочерний элемент управления, необязательную подсказку и сообщение проверки. В документированный контракт элемента управления входят id, aria-labelledby, aria-describedby, aria-invalid и aria-required. Элементы ввода Fluent можно размещать непосредственно внутри; пользовательские элементы управления могут получать свойства через функцию рендеринга. Поэтому полезнее обернуть Input для адреса электронной почты в Field, чем размещать произвольный текст над полем ввода.

В примере validationState принимает значение error только после отправки некорректной формы. Приложение проверяет формат адреса электронной почты и наличие дубликатов в списке, хранящемся в памяти. Введённое значение сохраняется, а validationMessage предлагает исправление. Field отображает результат и связывает его с полем ввода, но не определяет, существует ли адрес, не отправляет приглашение и не предоставляет доступ.

Прочитать вспомогательную спецификацию

### Отделяйте состояние проверки от разрешения доступа

В списке Select есть варианты Can view и Can edit, а у поля email предусмотрено собственное состояние ошибки. Не окрашивайте весь диалог в красный цвет из-за одного неполного адреса. required — это доступное для вспомогательных технологий обозначение, а не замена серверной авторизации или проверки данных. В примере формы указано noValidate, чтобы наглядно и единообразно показать собственное сообщение об ошибке; в рабочем сервисе запрос необходимо проверить повторно на сервере.

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

- Адрес уже есть в локальном списке доступа: покажите отдельное сообщение о дубликате, не добавляя ещё одну строку.

- Новый корректный адрес: добавьте выбранное разрешение локально, закройте диалог и честно сообщите, что результат сохранён только локально.

Прочитать вспомогательную спецификацию

## Дизайн-система Microsoft Fluent 2 Dialog: форма внутри поддерживаемой структуры компонентов

### Разместите кнопку отправки внутри формы

Структура состоит из Dialog, DialogTrigger, DialogSurface, элемента формы и DialogBody, содержащего DialogTitle, DialogContent и DialogActions. Форма оборачивает основную часть диалога, поэтому кнопка отправки внизу участвует в отправке формы. Для Cancel задан type="button"; кнопка находится внутри закрывающего DialogTrigger. Иначе тип кнопки по умолчанию может случайно отправить форму.

Параметры open и onOpenChange управляют видимостью. disableButtonEnhancement не позволяет триггеру повторно добавлять поведение кнопки компоненту Fluent Button, у которого оно уже есть. Обработчик успешной отправки закрывает диалог только после прохождения локальной проверки. При ошибке валидации адрес email и разрешение остаются доступными для исправления.

Прочитать вспомогательную спецификацию

### Выберите модальное поведение с учётом решения

Для этого краткого решения о доступе используется модальный тип по умолчанию. Немодальный диалог позволяет продолжить работу с фоном, а диалог предупреждения предназначен для срочных решений и имеет более строгие правила закрытия. Обычной форме доступа не нужна семантика предупреждения. Согласно официальным рекомендациям, фокус должен находиться внутри диалога, а после его закрытия возвращаться к триггеру; в автономной реализации используется нативный элемент dialog и явно восстанавливается фокус на триггере.

Прочитать вспомогательную спецификацию

## Дизайн-система Microsoft Fluent 2: пользовательские стили без нарушения темы

### Используйте makeStyles и семантические переменные

В полном примере кода makeStyles используется вместе с токенами для фона, интервалов и скругления. Благодаря этому пользовательская панель согласуется с темой, заданной провайдером. Сгенерированное имя класса — деталь реализации, поэтому не копируйте его из DevTools в таблицу стилей. Экспериментируя с темами, меняйте тему провайдера и проверяйте одновременно нативные элементы управления и пользовательские области.

Опубликованная шкала длительности анимации — от 50ms до 500ms. В примере HTML для простых изменений при наведении используется durationFast, 150ms; при настройке prefers-reduced-motion эти переходы отключаются. Это решение приложения с использованием доступного токена, а не утверждение, что каждое взаимодействие Fluent должно длиться 150ms. Содержимое и сообщения об ошибках остаются понятными и без анимации.

Прочитать вспомогательную спецификацию

### Проверьте ограничения выбранного способа реализации

Встроенный пример намеренно обозначен как HTML/CSS, а не как нативная сборка Fluent. Он показывает проверенные значения и взаимодействие с диалогом доступа без загрузки фреймворка в предварительном просмотре. Для оценки нативных стилей, порталов, управления фокусом и обновления компонентов используйте полный пример React. Успешное прохождение проверок взаимодействия в автономной реализации подтверждает только работоспособность этого примера, а не готовность производственного приложения для предоставления доступа.

Прочитать вспомогательную спецификацию

## Дизайн-система Microsoft Fluent 2: установите и проверьте полную композицию

### Рабочая точка входа, а не план задания

Инструкции предназначены для существующего приложения React с элементом, у которого id равен root. Установите набор v9, затем добавьте в приложение JSX-точку входа. Импорты, провайдер, дерево компонентов, обработчики, состояние и отрисовка в корневом элементе приведены вместе. Внешний сервис предоставления доступа API не вызывается. Если в стартовом проекте уже выполняется отрисовка App, сохраните существующую корневую точку входа и экспортируйте App вместо создания второго корневого элемента.

Зафиксируйте разрешённую версию пакета в lockfile. В ссылках на исходный код описана проверенная реализация React v9, тогда как содержимое URL master может меняться. Перед обновлением сравните экспортируемую тему и точное поведение Field, TabList и Dialog, используемое здесь. В примере намеренно не смешиваются компоненты @fluentui/react v8 и примеры v9.

1. Установите @fluentui/react-components@9 в существующий проект React и сохраните созданный lockfile.

2. Выполните отрисовку предоставленной точки входа внутри одного FluentProvider, установив webLightTheme начальной темой.

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

4. Повторите проверку с webDarkTheme и клавиатурной навигацией; проверьте связь панели с вкладкой, объявление ошибки и возврат фокуса.

Прочитать вспомогательную спецификацию

### Что нужно подключить в реальном продукте для предоставления доступа

Заменяйте локальное добавление только после определения контракта сервиса: авторизованный пользователь, целевое рабочее пространство, допустимые значения разрешений, результат приглашения и поведение при дубликате. Если сервис отклонит запрос, оставьте диалог открытым и покажите подтверждённый результат, а не сообщайте об успешном предоставлении доступа заранее. Эти правила не относятся к Fluent, но в коде явно показано место передачи запроса сервису, а не спрятано за универсальной кнопкой отправки.

Прочитать вспомогательную спецификацию

## Дизайн-система Microsoft Fluent 2: примеры компонентов

### Полноценный автономный интерактивный пример

Сохраните весь документ как index.html и откройте его в браузере. Автономная интерпретация HTML/CSS веб-токенов Fluent UI React v9 и документированной структуры компонентов Field, TabList и Dialog. Это не среда выполнения нативных компонентов Fluent.

```
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>Fluent web workspace study</title><style>
:root{color-scheme:light;--colorNeutralForeground1:#242424;--colorNeutralForeground2:#424242;--colorNeutralForeground3:#616161;--colorNeutralBackground1:#fff;--colorNeutralBackground2:#fafafa;--colorNeutralBackground3:#f5f5f5;--colorNeutralStroke1:#d1d1d1;--colorNeutralStroke2:#e0e0e0;--colorBrandBackground:#0f6cbd;--colorBrandBackgroundHover:#115ea3;--colorBrandBackgroundPressed:#0c3b5e;--colorBrandBackground2:#ebf3fc;--colorBrandForegroundLink:#115ea3;--colorNeutralForegroundOnBrand:#fff;--borderRadiusMedium:4px;--borderRadiusXLarge:8px;--spacingHorizontalS:8px;--spacingHorizontalM:12px;--spacingHorizontalL:16px;--spacingHorizontalXXL:24px;--durationFast:150ms}
[data-theme=dark]{color-scheme:dark;--colorNeutralForeground1:#fff;--colorNeutralForeground2:#d6d6d6;--colorNeutralForeground3:#adadad;--colorNeutralBackground1:#292929;--colorNeutralBackground2:#1f1f1f;--colorNeutralBackground3:#141414;--colorNeutralStroke1:#666;--colorNeutralStroke2:#525252;--colorBrandBackground:#115ea3;--colorBrandBackgroundHover:#0f6cbd;--colorBrandBackground2:#082338;--colorBrandForegroundLink:#479ef5}
*{box-sizing:border-box}body{margin:0;background:var(--colorNeutralBackground3);color:var(--colorNeutralForeground1);font-family:'Segoe UI','Segoe UI Web (West European)',-apple-system,BlinkMacSystemFont,Roboto,'Helvetica Neue',sans-serif;font-size:14px;line-height:20px}button,input,select{font:inherit}button{cursor:pointer;min-height:32px;padding:5px 12px;border:1px solid var(--colorNeutralStroke1);border-radius:var(--borderRadiusMedium);background:var(--colorNeutralBackground1);color:var(--colorNeutralForeground1);font-weight:600;transition:background var(--durationFast)}button:hover{background:var(--colorNeutralBackground3)}button:focus-visible,a:focus-visible,input:focus-visible,select:focus-visible,[tabindex]:focus-visible{outline:2px solid var(--colorBrandForegroundLink);outline-offset:3px}.primary{background:var(--colorBrandBackground);color:var(--colorNeutralForegroundOnBrand);border-color:transparent}.primary:hover{background:var(--colorBrandBackgroundHover)}.primary:active{background:var(--colorBrandBackgroundPressed)}.topbar{display:flex;justify-content:space-between;align-items:center;padding:12px 24px;background:var(--colorNeutralBackground1);border-bottom:1px solid var(--colorNeutralStroke2);gap:16px}.brand{display:flex;align-items:center;gap:12px;font-weight:600}.mark{display:grid;grid-template-columns:repeat(2,7px);gap:3px}.mark i{width:7px;height:7px;background:var(--colorBrandBackground)}.top-note{font-size:12px;color:var(--colorNeutralForeground3)}.page{max-width:1100px;margin:auto;padding:24px}.heading{display:flex;justify-content:space-between;align-items:center;gap:16px;margin-bottom:16px}.eyebrow{font-size:12px;line-height:16px;color:var(--colorNeutralForeground3);margin-bottom:4px}h1{font-size:24px;line-height:32px;font-weight:600;margin:0 0 4px}h2{font-size:20px;line-height:28px;font-weight:600;margin:0 0 16px}h3{font-size:16px;line-height:22px;font-weight:600;margin:20px 0 8px}p{margin:0 0 12px}.muted{color:var(--colorNeutralForeground3)}.tabs{display:flex;gap:16px;border-bottom:1px solid var(--colorNeutralStroke2);margin-bottom:24px}.tab{position:relative;background:none;border:0;border-radius:0;font-weight:400;padding:12px 4px;color:var(--colorNeutralForeground2)}.tab[aria-selected=true]{font-weight:600;color:var(--colorBrandForegroundLink)}.tab[aria-selected=true]:after{content:'';position:absolute;bottom:0;left:4px;right:4px;height:3px;background:var(--colorBrandBackground);border-radius:3px}.layout{display:grid;grid-template-columns:minmax(0,1fr) 240px;gap:24px}.document,.aside,.people{background:var(--colorNeutralBackground1);border:1px solid var(--colorNeutralStroke2);border-radius:var(--borderRadiusXLarge);padding:24px}.document{max-width:720px}.docmeta{display:flex;gap:8px;align-items:center;color:var(--colorNeutralForeground3);font-size:12px;line-height:16px;margin-bottom:24px}.avatar{flex:none;display:grid;place-items:center;background:var(--colorBrandBackground2);color:var(--colorBrandForegroundLink);font-weight:600;width:32px;height:32px;border-radius:50%}.quote{padding:12px 16px;background:var(--colorNeutralBackground2);border-left:3px solid var(--colorBrandBackground);margin:16px 0}.checklist{list-style:none;padding:0;margin:12px 0}.checklist li{display:flex;gap:8px;margin:8px 0}.checklist input{accent-color:var(--colorBrandBackground);width:16px;height:16px;margin-top:2px}.aside{padding:20px;height:fit-content}.aside h2{font-size:16px;line-height:22px;margin-bottom:16px}.person{display:flex;gap:8px;align-items:center;margin-bottom:16px}.person p{margin:0}.small{font-size:12px;line-height:16px}.aside hr{border:0;border-top:1px solid var(--colorNeutralStroke2);margin:20px 0}.tag{display:inline-block;border:1px solid var(--colorNeutralStroke1);padding:2px 8px;border-radius:4px;font-size:12px;color:var(--colorNeutralForeground2)}.people-head{display:flex;align-items:center;justify-content:space-between;margin-bottom:16px;gap:12px}.people-head h2{margin:0}.people ul{list-style:none;padding:0;margin:0}.member{display:flex;align-items:center;gap:12px;padding:16px 0;border-top:1px solid var(--colorNeutralStroke2)}.member-info{flex:1;min-width:0;overflow-wrap:anywhere}.member-info p{margin:0}.member .tag{flex:none}.caption{margin:20px 0 0;color:var(--colorNeutralForeground3);font-size:12px;line-height:16px}.status{margin-top:16px;overflow-wrap:anywhere;color:var(--colorNeutralForeground2)}.status:empty{display:none}[hidden]{display:none!important}dialog{width:min(520px,calc(100vw - 32px));padding:24px;border:1px solid var(--colorNeutralStroke1);border-radius:8px;background:var(--colorNeutralBackground1);color:var(--colorNeutralForeground1);max-height:calc(100dvh - 32px);overflow:auto}dialog::backdrop{background:rgba(0,0,0,.4)}dialog h2{font-size:20px;line-height:28px;margin-bottom:12px}.field{margin:20px 0}.field label{display:block;font-weight:600;margin-bottom:8px}.required{color:#b10e1e}.field input,.field select{width:100%;min-height:36px;background:var(--colorNeutralBackground1);color:var(--colorNeutralForeground1);padding:7px 12px;border:1px solid var(--colorNeutralStroke1);border-bottom-color:var(--colorNeutralForeground3);border-radius:4px}.field input:focus{border-bottom:2px solid var(--colorBrandBackground);padding-bottom:6px}.hint{font-size:12px;line-height:16px;color:var(--colorNeutralForeground3);margin-top:8px}.error{font-size:12px;line-height:16px;color:#b10e1e;margin-top:8px}.field input[aria-invalid=true]{border-color:#b10e1e}.actions{display:flex;justify-content:flex-end;gap:8px;margin-top:24px}[data-theme=dark] .error,[data-theme=dark] .required{color:#ff99a4}footer{padding:0 24px 20px;text-align:center;font-size:12px;color:var(--colorNeutralForeground3)}@media(max-width:700px){.page{padding:16px}.topbar{padding:12px 16px}.top-note{display:none}.layout{grid-template-columns:1fr}.document,.people{padding:20px}.aside{display:grid;grid-template-columns:1fr 1fr;gap:8px 16px}.aside h2,.aside hr{grid-column:1/-1}.aside hr{margin:0}.heading{align-items:flex-start}.heading .primary{flex:none}.member{flex-wrap:wrap}.member .tag{margin-left:44px}.topbar button,.heading button{min-height:40px}}@media(max-width:420px){h1{font-size:20px;line-height:28px}.heading{flex-direction:column;gap:12px}.aside{display:block}.member .tag{margin-left:0}}@media(prefers-reduced-motion:reduce){*{transition:none!important}}
</style></head><body>
<header class="topbar"><div class="brand"><span class="mark" aria-hidden="true"><i></i><i></i><i></i><i></i></span>Northstar workspace <span class="top-note">/ Product team</span></div><button id="theme" aria-pressed="false">Dark theme</button></header>
<main class="page"><div class="heading"><div><div class="eyebrow">Team workspace · October launch</div><h1>Launch workspace</h1><p class="muted">One brief. A clear decision. The right people.</p></div><button class="primary" id="share">Add a person</button></div>
<div class="tabs" role="tablist" aria-label="Workspace views"><button class="tab" id="brief-tab" role="tab" aria-controls="brief-panel" aria-selected="true" tabindex="0">Brief</button><button class="tab" id="people-tab" role="tab" aria-controls="people-panel" aria-selected="false" tabindex="-1">People <span id="count">2</span></button></div>
<section id="brief-panel" role="tabpanel" aria-labelledby="brief-tab" tabindex="0"><div class="layout"><article class="document"><div class="docmeta"><span class="avatar" aria-hidden="true">AC</span><span>Alex Chen · Product design<br>Updated today at 09:30</span></div><h2>A more welcoming first project</h2><p>Help a new team move from an empty workspace to its first useful result. This review focuses on the welcome screen and the first-project setup.</p><h3>The decision</h3><div class="quote">Should a sample project be created automatically, or offered as an option?</div><p>We want a clear starting point without filling the workspace with content someone did not choose. Review both paths before the team walkthrough.</p><h3>Review checklist</h3><ul class="checklist"><li><input id="c1" type="checkbox" checked><label for="c1">Welcome copy explains the next step</label></li><li><input id="c2" type="checkbox"><label for="c2">Empty workspace offers a sample project</label></li><li><input id="c3" type="checkbox"><label for="c3">Keyboard path reaches the first project action</label></li></ul><p class="small muted">Checklist changes are local to this example.</p></article><aside class="aside"><h2>Working together</h2><div class="person"><span class="avatar" aria-hidden="true">AC</span><div><p>Alex Chen</p><span class="small muted">Workspace owner</span></div></div><div class="person"><span class="avatar" aria-hidden="true">MP</span><div><p>Maya Patel</p><span class="small muted">Can edit</span></div></div><hr><div><h3>Next review</h3><p>Thursday, 10:00<br><span class="muted small">Team walkthrough · UTC</span></p></div><div><h3>Access</h3><span class="tag">Specific people</span><p class="small muted" style="margin-top:8px">Adding a person here updates only this local preview.</p></div></aside></div></section>
<section id="people-panel" role="tabpanel" aria-labelledby="people-tab" tabindex="0" hidden><div class="people"><div class="people-head"><h2>People with access</h2><span class="tag">Local example</span></div><p class="muted">The roles below are fictional. No invitation or real access change is made.</p><ul id="members"><li class="member"><span class="avatar" aria-hidden="true">AC</span><div class="member-info"><p>Alex Chen</p><span class="small muted">alex@example.com</span></div><span class="tag">Owner</span></li><li class="member"><span class="avatar" aria-hidden="true">MP</span><div class="member-info"><p>Maya Patel</p><span class="small muted">maya@example.com</span></div><span class="tag">Can edit</span></li></ul></div></section><p class="status" id="status" role="status"></p><p class="caption">Fluent UI React v9 web-token study · HTML/CSS interpretation, not native library components. Try tabs, theme, checkboxes and the access form.</p></main>
<dialog id="access" aria-labelledby="dialog-title"><form id="access-form" novalidate><h2 id="dialog-title">Add a person to Launch workspace</h2><p class="muted">Choose an address and permission. This demonstration updates the local list only.</p><div class="field"><label for="email">Email address <span class="required" aria-hidden="true">*</span></label><input id="email" type="email" autocomplete="email" aria-required="true" aria-describedby="email-hint" autofocus><div class="hint" id="email-hint">For example, jordan@example.com</div><div class="error" id="error" role="alert" hidden></div></div><div class="field"><label for="permission">Permission</label><select id="permission" aria-describedby="permission-hint"><option>Can view</option><option>Can edit</option></select><div class="hint" id="permission-hint">Viewers can read; editors can change the brief in a real application</div></div><div class="actions"><button type="button" id="cancel">Cancel</button><button class="primary" type="submit">Add locally</button></div></form></dialog>
<script>
const $=id=>document.getElementById(id),tabs=[...document.querySelectorAll('[role=tab]')],emails=new Set(['alex@example.com','maya@example.com']);
function selectTab(tab){tabs.forEach(t=>{const chosen=t===tab;t.setAttribute('aria-selected',String(chosen));t.tabIndex=chosen?0:-1;$(t.getAttribute('aria-controls')).hidden=!chosen;});}
tabs.forEach((tab,index)=>{tab.addEventListener('click',()=>selectTab(tab));tab.addEventListener('keydown',event=>{let next;if(event.key==='ArrowRight')next=(index+1)%tabs.length;if(event.key==='ArrowLeft')next=(index+tabs.length-1)%tabs.length;if(event.key==='Home')next=0;if(event.key==='End')next=tabs.length-1;if(next!==undefined){event.preventDefault();tabs.forEach(t=>t.tabIndex=-1);tabs[next].tabIndex=0;tabs[next].focus();}});});
$('theme').onclick=()=>{const dark=document.documentElement.dataset.theme!=='dark';document.documentElement.dataset.theme=dark?'dark':'light';$('theme').textContent=dark?'Light theme':'Dark theme';$('theme').setAttribute('aria-pressed',String(dark));};
const dialog=$('access'),email=$('email'),error=$('error');function clearError(){error.hidden=true;error.textContent='';email.removeAttribute('aria-invalid');email.setAttribute('aria-describedby','email-hint');}function showError(message){error.textContent=message;error.hidden=false;email.setAttribute('aria-invalid','true');email.setAttribute('aria-describedby','email-hint error');email.focus();}
$('share').onclick=()=>{clearError();dialog.showModal();email.focus();};$('cancel').onclick=()=>dialog.close();dialog.addEventListener('close',()=>$('share').focus());dialog.addEventListener('click',event=>{if(event.target===dialog){const r=dialog.getBoundingClientRect();if(event.clientX<r.left||event.clientX>r.right||event.clientY<r.top||event.clientY>r.bottom)dialog.close();}});email.addEventListener('input',clearError);
$('access-form').addEventListener('submit',event=>{event.preventDefault();const value=email.value.trim().toLowerCase();if(!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)){showError('Enter an email address such as name@example.com');return;}if(emails.has(value)){showError('This address already appears in the local access list');return;}const permission=$('permission').value;emails.add(value);const item=document.createElement('li');item.className='member';const avatar=document.createElement('span');avatar.className='avatar';avatar.setAttribute('aria-hidden','true');avatar.textContent=value.slice(0,2).toUpperCase();const info=document.createElement('div');info.className='member-info';const name=document.createElement('p');name.textContent=value;const sub=document.createElement('span');sub.className='small muted';sub.textContent='Added in this local example';info.append(name,sub);const role=document.createElement('span');role.className='tag';role.textContent=permission;item.append(avatar,info,role);$('members').append(item);$('count').textContent=emails.size;$('status').textContent='Added '+value+' as '+permission.toLowerCase()+' locally. No invitation was sent.';email.value='';clearError();selectTab($('people-tab'));dialog.close();});
</script></body></html>
```

Справочник по коду

- Диалог доступа и вкладки в этом документе используют нативные элементы управления HTML; чтобы оценить поведение FluentProvider, Field и Dialog, запустите отдельную точку входа React v9.

### Полный пример нативной реализации

Полная реализация из раздела настройки собрана здесь вместе с пояснениями к отдельным частям API. Следуйте документированным требованиям к пакетам и сборке из раздела настройки.

```
// Terminal: npm install @fluentui/react-components@9
// Existing React application; index.html must contain <div id="root"></div>.
// Entry file: src/main.jsx
import * as React from 'react';
import { createRoot } from 'react-dom/client';
import {
  FluentProvider, webLightTheme, webDarkTheme, makeStyles, tokens,
  Button, Body1, Title3, TabList, Tab, Field, Input, Select,
  Dialog, DialogTrigger, DialogSurface, DialogBody, DialogTitle,
  DialogContent, DialogActions
} from '@fluentui/react-components';

const useStyles = makeStyles({
  page: { maxWidth: '880px', margin: '0 auto', padding: tokens.spacingHorizontalXXL },
  header: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: tokens.spacingHorizontalL },
  panel: { padding: tokens.spacingHorizontalXXL, marginTop: tokens.spacingVerticalL,
    backgroundColor: tokens.colorNeutralBackground2, borderRadius: tokens.borderRadiusXLarge },
  form: { display: 'grid', gap: tokens.spacingVerticalL },
  row: { display: 'flex', flexWrap: 'wrap', justifyContent: 'space-between', gap: tokens.spacingHorizontalM,
    paddingTop: tokens.spacingVerticalM, paddingBottom: tokens.spacingVerticalM },
});

function Workspace({ dark, setDark }) {
  const styles = useStyles();
  const [tab, setTab] = React.useState('brief');
  const [open, setOpen] = React.useState(false);
  const [email, setEmail] = React.useState('');
  const [permission, setPermission] = React.useState('Can view');
  const [error, setError] = React.useState('');
  const [notice, setNotice] = React.useState('');
  const [people, setPeople] = React.useState([{ email: 'alex@example.com', permission: 'Owner' }]);
  function submit(event) {
    event.preventDefault();
    const value = email.trim().toLowerCase();
    if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) {
      setError('Enter an email address such as name@example.com'); return;
    }
    if (people.some(person => person.email === value)) {
      setError('This address already appears in the local access list'); return;
    }
    setPeople(previous => [...previous, { email: value, permission }]);
    setNotice(`Added ${value} as ${permission.toLowerCase()} in this local example. No invitation was sent.`);
    setEmail(''); setError(''); setOpen(false); setTab('people');
  }
  return <main className={styles.page}>
    <div className={styles.header}>
      <Title3 as="h1">Launch workspace</Title3>
      <Button onClick={() => setDark(!dark)}>{dark ? 'Light theme' : 'Dark theme'}</Button>
    </div>
    <Body1>Project brief and access settings · local demonstration</Body1>
    <TabList aria-label="Workspace views" selectedValue={tab}
      onTabSelect={(_, data) => setTab(data.value)}>
      <Tab id="brief-tab" value="brief" aria-controls="brief-panel">Brief</Tab>
      <Tab id="people-tab" value="people" aria-controls="people-panel">People</Tab>
    </TabList>
    <section id="brief-panel" role="tabpanel" aria-labelledby="brief-tab"
      hidden={tab !== 'brief'} className={styles.panel} tabIndex={0}>
      <h2>Launch a clearer onboarding flow</h2>
      <p>Review the welcome screen and first-project setup before the team walkthrough.</p>
      <p>Decision needed: should the sample project be created automatically or offered as an option?</p>
    </section>
    <section id="people-panel" role="tabpanel" aria-labelledby="people-tab"
      hidden={tab !== 'people'} className={styles.panel} tabIndex={0}>
      <h2>People with access</h2>
      <ul>{people.map(person => <li key={person.email} className={styles.row}>
        <span>{person.email}</span><strong>{person.permission}</strong>
      </li>)}</ul>
    </section>
    <Dialog open={open} onOpenChange={(_, data) => setOpen(data.open)}>
      <DialogTrigger disableButtonEnhancement><Button appearance="primary">Add a person</Button></DialogTrigger>
      <DialogSurface>
        <form noValidate onSubmit={submit}>
          <DialogBody>
            <DialogTitle>Add a person to Launch workspace</DialogTitle>
            <DialogContent className={styles.form}>
              <p>This example updates local state only. It does not send email or change real access.</p>
              <Field label="Email address" required validationState={error ? 'error' : 'none'}
                validationMessage={error || undefined}>
                <Input type="email" value={email} onChange={(_, data) => { setEmail(data.value); setError(''); }} />
              </Field>
              <Field label="Permission" hint="Viewers can read; editors can change the brief in a real application">
                <Select value={permission} onChange={(_, data) => setPermission(data.value)}>
                  <option>Can view</option><option>Can edit</option>
                </Select>
              </Field>
            </DialogContent>
            <DialogActions>
              <DialogTrigger disableButtonEnhancement><Button type="button">Cancel</Button></DialogTrigger>
              <Button type="submit" appearance="primary">Add locally</Button>
            </DialogActions>
          </DialogBody>
        </form>
      </DialogSurface>
    </Dialog>
    <p role="status">{notice}</p>
  </main>;
}
function App() {
  const [dark, setDark] = React.useState(false);
  return <FluentProvider theme={dark ? webDarkTheme : webLightTheme}>
    <Workspace dark={dark} setDark={setDark} />
  </FluentProvider>;
}
createRoot(document.getElementById('root')).render(<App />);
```

Справочник по коду

- Полная композиция компонентов React v9 для существующего приложения React. Автономный предварительный просмотр — отдельная интерпретация HTML/CSS. Этот код выполняет только локальную проверку данных и обновление состояния; отправки email и реального изменения разрешений не происходит. API исходного кода проверены; совместимость нативного пакета при компиляции не гарантируется до проверки в целевом проекте.

### Field + Input + Select: единая форма доступа с метками

В этих фрагментах используются переменные состояния и импорты из полной конфигурации ниже. Field отвечает за отображение метки, подсказки и результата проверки; приложение — за правило проверки адреса email и поиск дубликатов.

```
<Field label="Email address" required
  validationState={error ? 'error' : 'none'}
  validationMessage={error || undefined}>
  <Input type="email" value={email}
    onChange={(_, data) => { setEmail(data.value); setError(''); }} />
</Field>
<Field label="Permission" hint="Choose what this person can do">
  <Select value={permission}
    onChange={(_, data) => setPermission(data.value)}>
    <option>Can view</option><option>Can edit</option>
  </Select>
</Field>
```

Справочник по коду

- required обозначает обязательное поле для вспомогательных технологий, но не реализует правило проверки данных приложения.

- Если задан validationMessage, по умолчанию состоянием проверки становится error. Явно установите none, если ошибку показывать не нужно.

- Обработчик Input onChange передаёт текущую строку в data.value; не путайте его с обработчиком, который получает только событие DOM.

### Управляемый TabList с явно связанными панелями

TabList управляет выбором и поведением вкладок. Панели содержимого отрисовывайте самостоятельно, связывая их id и доступные имена с соответствующими Tab.

```
<TabList selectedValue={tab} aria-label="Workspace views"
  onTabSelect={(_, data) => setTab(data.value)}>
  <Tab id="brief-tab" value="brief" aria-controls="brief-panel">Brief</Tab>
  <Tab id="people-tab" value="people" aria-controls="people-panel">People</Tab>
</TabList>
<section id="brief-panel" role="tabpanel" aria-labelledby="brief-tab"
  hidden={tab !== 'brief'} tabIndex={0}>Project brief</section>
<section id="people-panel" role="tabpanel" aria-labelledby="people-tab"
  hidden={tab !== 'people'} tabIndex={0}>People with access</section>
```

Справочник по коду

- Параметр selectedValue задаётся для TabList; у каждого Tab должно быть уникальное значение.

- По умолчанию selectTabOnFocus равен false. Перемещение фокуса стрелками и активация вкладки — отдельные действия, если это поведение намеренно не изменено.

- Горизонтальные вкладки Fluent не переносятся на новую строку и не прокручиваются автоматически; в примере используются две короткие метки, которые остаются видимыми.

## Дизайн-система Microsoft Fluent 2 Настройка агента

### Отправная точка для реализации

Полная композиция компонентов React v9 для существующего приложения React. Автономный предварительный просмотр — отдельная интерпретация HTML/CSS. Этот код выполняет только локальную проверку данных и обновление состояния; отправки email и реального изменения разрешений не происходит. API исходного кода проверены; совместимость нативного пакета при компиляции не гарантируется до проверки в целевом проекте.

```
// Terminal: npm install @fluentui/react-components@9
// Existing React application; index.html must contain <div id="root"></div>.
// Entry file: src/main.jsx
import * as React from 'react';
import { createRoot } from 'react-dom/client';
import {
  FluentProvider, webLightTheme, webDarkTheme, makeStyles, tokens,
  Button, Body1, Title3, TabList, Tab, Field, Input, Select,
  Dialog, DialogTrigger, DialogSurface, DialogBody, DialogTitle,
  DialogContent, DialogActions
} from '@fluentui/react-components';

const useStyles = makeStyles({
  page: { maxWidth: '880px', margin: '0 auto', padding: tokens.spacingHorizontalXXL },
  header: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: tokens.spacingHorizontalL },
  panel: { padding: tokens.spacingHorizontalXXL, marginTop: tokens.spacingVerticalL,
    backgroundColor: tokens.colorNeutralBackground2, borderRadius: tokens.borderRadiusXLarge },
  form: { display: 'grid', gap: tokens.spacingVerticalL },
  row: { display: 'flex', flexWrap: 'wrap', justifyContent: 'space-between', gap: tokens.spacingHorizontalM,
    paddingTop: tokens.spacingVerticalM, paddingBottom: tokens.spacingVerticalM },
});

function Workspace({ dark, setDark }) {
  const styles = useStyles();
  const [tab, setTab] = React.useState('brief');
  const [open, setOpen] = React.useState(false);
  const [email, setEmail] = React.useState('');
  const [permission, setPermission] = React.useState('Can view');
  const [error, setError] = React.useState('');
  const [notice, setNotice] = React.useState('');
  const [people, setPeople] = React.useState([{ email: 'alex@example.com', permission: 'Owner' }]);
  function submit(event) {
    event.preventDefault();
    const value = email.trim().toLowerCase();
    if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) {
      setError('Enter an email address such as name@example.com'); return;
    }
    if (people.some(person => person.email === value)) {
      setError('This address already appears in the local access list'); return;
    }
    setPeople(previous => [...previous, { email: value, permission }]);
    setNotice(`Added ${value} as ${permission.toLowerCase()} in this local example. No invitation was sent.`);
    setEmail(''); setError(''); setOpen(false); setTab('people');
  }
  return <main className={styles.page}>
    <div className={styles.header}>
      <Title3 as="h1">Launch workspace</Title3>
      <Button onClick={() => setDark(!dark)}>{dark ? 'Light theme' : 'Dark theme'}</Button>
    </div>
    <Body1>Project brief and access settings · local demonstration</Body1>
    <TabList aria-label="Workspace views" selectedValue={tab}
      onTabSelect={(_, data) => setTab(data.value)}>
      <Tab id="brief-tab" value="brief" aria-controls="brief-panel">Brief</Tab>
      <Tab id="people-tab" value="people" aria-controls="people-panel">People</Tab>
    </TabList>
    <section id="brief-panel" role="tabpanel" aria-labelledby="brief-tab"
      hidden={tab !== 'brief'} className={styles.panel} tabIndex={0}>
      <h2>Launch a clearer onboarding flow</h2>
      <p>Review the welcome screen and first-project setup before the team walkthrough.</p>
      <p>Decision needed: should the sample project be created automatically or offered as an option?</p>
    </section>
    <section id="people-panel" role="tabpanel" aria-labelledby="people-tab"
      hidden={tab !== 'people'} className={styles.panel} tabIndex={0}>
      <h2>People with access</h2>
      <ul>{people.map(person => <li key={person.email} className={styles.row}>
        <span>{person.email}</span><strong>{person.permission}</strong>
      </li>)}</ul>
    </section>
    <Dialog open={open} onOpenChange={(_, data) => setOpen(data.open)}>
      <DialogTrigger disableButtonEnhancement><Button appearance="primary">Add a person</Button></DialogTrigger>
      <DialogSurface>
        <form noValidate onSubmit={submit}>
          <DialogBody>
            <DialogTitle>Add a person to Launch workspace</DialogTitle>
            <DialogContent className={styles.form}>
              <p>This example updates local state only. It does not send email or change real access.</p>
              <Field label="Email address" required validationState={error ? 'error' : 'none'}
                validationMessage={error || undefined}>
                <Input type="email" value={email} onChange={(_, data) => { setEmail(data.value); setError(''); }} />
              </Field>
              <Field label="Permission" hint="Viewers can read; editors can change the brief in a real application">
                <Select value={permission} onChange={(_, data) => setPermission(data.value)}>
                  <option>Can view</option><option>Can edit</option>
                </Select>
              </Field>
            </DialogContent>
            <DialogActions>
              <DialogTrigger disableButtonEnhancement><Button type="button">Cancel</Button></DialogTrigger>
              <Button type="submit" appearance="primary">Add locally</Button>
            </DialogActions>
          </DialogBody>
        </form>
      </DialogSurface>
    </Dialog>
    <p role="status">{notice}</p>
  </main>;
}
function App() {
  const [dark, setDark] = React.useState(false);
  return <FluentProvider theme={dark ? webDarkTheme : webLightTheme}>
    <Workspace dark={dark} setDark={setDark} />
  </FluentProvider>;
}
createRoot(document.getElementById('root')).render(<App />);
```

### Запрос для вашего проекта

```
Используйте Fluent UI React v9 из @fluentui/react-components. Отрисуйте FluentProvider с webLightTheme и переключателем webDarkTheme. Составьте краткое описание с помощью Title3 и Body1, управляемого TabList с представлениями вкладок и явно заданными панелями, а также Dialog доступа с Field/Input/Select и настоящей формой. Для пользовательских фонов и интервалов используйте токены. Следуйте предоставленному примеру компонентов и парам типографических значений времени выполнения, в том числе Subtitle1 со значениями 20/28. Сохраняйте некорректный ввод, отклоняйте локальные дубликаты адресов, возвращайте фокус после закрытия диалога и честно описывайте локальные добавления. Подключайте сервис предоставления доступа, только если определён контракт авторизации и результата.
```

## Дизайн-система Microsoft Fluent 2: источники и область применения

Пример приложения OpenDesign с использованием Fluent UI React v9, а не продукт Microsoft и не действующий сервис предоставления доступа. Автономный предварительный просмотр — интерпретация HTML/CSS проверенных веб-токенов и структуры компонентов; в приведённом ниже коде React используются нативные компоненты Fluent. Справочные значения соответствуют версии исходного кода по ссылке, проверенной 3 октября 2026 года, и не гарантируют, что во всех выпусках, платформах и брендах значения будут одинаковыми.

- Точка входа пакета React v9 Установка провайдера и набора компонентов.

- Архитектура темы Использование семантических токенов и доступные темы.

- Типографика времени выполнения Стек шрифтов, размер, высота строки и начертания.

- Соответствие стилей текста Предустановки, связанные с глобальными токенами.

- Шкала типографики дизайн-языка Таблица дизайн-языка; Subtitle1 отличается от пары значений времени выполнения.

- Интервалы времени выполнения Названия горизонтальных и вертикальных токенов.

- Рекомендации по компоновке Глобальные интервалы и адаптивность.

- Токены скругления Доступные значения формы во время выполнения.

- Длительности анимации Значения длительности времени выполнения.

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

- Сопоставление семантических значений для тёмной темы Назначения псевдонимов для тёмной темы.

- Палитры брендов Фактические значения брендов для Web и Teams.

- Создание темы Web webLightTheme использует brandWeb.

- Использование Field Рекомендации по меткам, проверке данных и композиции.

- Field API Слоты и контракт для доступных дочерних элементов.

- Использование TabList Рекомендации по компоновке и выбору категории.

- TabList API Управляемый выбор и параметры управления фокусом.

- Использование Dialog Рекомендации по размещению формы и модальному взаимодействию.

- Dialog API Управление видимостью и типы модальных окон.

## Связанные ресурсы дизайн-системы Microsoft Fluent 2

### Monday Vibe

Создайте рабочую доску доставки с токенами Vibe 4 и элементами управления React. Разберитесь, чем семантические цвета действий отличаются от меток на доске, изучите именованные шаги отступов и сокращённые записи шрифтов, а также настройте CSS, необходимую новому приложению.

### Morningstar дизайн-система

Создайте исследовательский интерфейс с выровненными числами, осмысленными цветами ресурсов и удобной для чтения типографикой. Изучите константы MDS, переход на Vue 3, переход на Morningstar Intrinsic и сортируемый блокнот распределения активов на вымышленных данных.

### Mozilla Protocol

Редакционное руководство по Protocol с настоящими гарнитурами, шкалами отступов и ширины контейнера, адаптивной иллюстрированной страницей для чтения и полноценным примером формы на нативном CSS.


- [Справочник по реализации](https://github.com/microsoft/fluentui/blob/master/packages/react-components/react-components/README.md)
- [Источник](https://github.com/microsoft/fluentui/blob/master/packages/tokens/src/alias/lightColor.ts)
- [Источник](https://github.com/microsoft/fluentui/blob/master/packages/tokens/src/global/brandColors.ts)
- [Источник](https://github.com/microsoft/fluentui/blob/master/packages/tokens/src/global/spacings.ts)
- [Источник](https://github.com/microsoft/fluentui/blob/master/packages/tokens/src/global/fonts.ts)
- [Источник](https://github.com/microsoft/fluentui/blob/master/packages/tokens/src/global/borderRadius.ts)
- [Источник](https://github.com/microsoft/fluentui/blob/master/packages/tokens/src/global/durations.ts)
- [Источник](https://github.com/microsoft/fluentui/blob/master/packages/tokens/src/alias/darkColor.ts)
- [Прочитать вспомогательную спецификацию](https://github.com/microsoft/fluentui/blob/master/packages/react-components/react-components/README.md)
- [Прочитать вспомогательную спецификацию](https://github.com/microsoft/fluentui/blob/master/docs/architecture/design-tokens.md)
- [Прочитать вспомогательную спецификацию](https://github.com/microsoft/fluentui/blob/master/packages/tokens/src/alias/lightColor.ts)
- [Прочитать вспомогательную спецификацию](https://github.com/microsoft/fluentui/blob/master/packages/tokens/src/alias/darkColor.ts)
- [Прочитать вспомогательную спецификацию](https://github.com/microsoft/fluentui/blob/master/packages/tokens/src/global/fonts.ts)
- [Прочитать вспомогательную спецификацию](https://github.com/microsoft/fluentui/blob/master/packages/tokens/src/global/typographyStyles.ts)
- [Прочитать вспомогательную спецификацию](https://github.com/microsoft/fluentui/blob/master/packages/tokens/src/global/spacings.ts)
- [Прочитать вспомогательную спецификацию](https://github.com/microsoft/fluentui/blob/master/packages/tokens/src/global/borderRadius.ts)
- [Прочитать вспомогательную спецификацию](https://github.com/microsoft/fluentui/blob/master/packages/react-components/react-tabs/library/src/components/TabList/TabList.types.ts)
- [Прочитать вспомогательную спецификацию](https://fluent2.microsoft.design/components/web/react/core/tablist/usage)
- [Прочитать вспомогательную спецификацию](https://github.com/microsoft/fluentui/blob/master/packages/react-components/react-field/library/src/components/Field/Field.types.ts)
- [Прочитать вспомогательную спецификацию](https://fluent2.microsoft.design/components/web/react/core/field/usage)
- [Прочитать вспомогательную спецификацию](https://github.com/microsoft/fluentui/blob/master/packages/react-components/react-dialog/library/src/components/Dialog/Dialog.types.ts)
- [Прочитать вспомогательную спецификацию](https://fluent2.microsoft.design/components/web/react/core/dialog/usage)
- [Прочитать вспомогательную спецификацию](https://github.com/microsoft/fluentui/blob/master/packages/tokens/src/global/durations.ts)
- [Справочник по коду](https://github.com/microsoft/fluentui/blob/master/packages/react-components/react-components/README.md)
- [Справочник по коду](https://github.com/microsoft/fluentui/blob/master/packages/react-components/react-field/library/src/components/Field/Field.types.ts)
- [Справочник по коду](https://github.com/microsoft/fluentui/blob/master/packages/react-components/react-tabs/library/src/components/TabList/TabList.types.ts)
- [Точка входа пакета React v9 Установка провайдера и набора компонентов.](https://github.com/microsoft/fluentui/blob/master/packages/react-components/react-components/README.md)
- [Архитектура темы Использование семантических токенов и доступные темы.](https://github.com/microsoft/fluentui/blob/master/docs/architecture/design-tokens.md)
- [Типографика времени выполнения Стек шрифтов, размер, высота строки и начертания.](https://github.com/microsoft/fluentui/blob/master/packages/tokens/src/global/fonts.ts)
- [Соответствие стилей текста Предустановки, связанные с глобальными токенами.](https://github.com/microsoft/fluentui/blob/master/packages/tokens/src/global/typographyStyles.ts)
- [Шкала типографики дизайн-языка Таблица дизайн-языка; Subtitle1 отличается от пары значений времени выполнения.](https://fluent2.microsoft.design/typography)
- [Интервалы времени выполнения Названия горизонтальных и вертикальных токенов.](https://github.com/microsoft/fluentui/blob/master/packages/tokens/src/global/spacings.ts)
- [Рекомендации по компоновке Глобальные интервалы и адаптивность.](https://fluent2.microsoft.design/layout)
- [Токены скругления Доступные значения формы во время выполнения.](https://github.com/microsoft/fluentui/blob/master/packages/tokens/src/global/borderRadius.ts)
- [Длительности анимации Значения длительности времени выполнения.](https://github.com/microsoft/fluentui/blob/master/packages/tokens/src/global/durations.ts)
- [Сопоставление семантических значений для светлой темы Назначения нейтральных и брендовых псевдонимов.](https://github.com/microsoft/fluentui/blob/master/packages/tokens/src/alias/lightColor.ts)
- [Сопоставление семантических значений для тёмной темы Назначения псевдонимов для тёмной темы.](https://github.com/microsoft/fluentui/blob/master/packages/tokens/src/alias/darkColor.ts)
- [Палитры брендов Фактические значения брендов для Web и Teams.](https://github.com/microsoft/fluentui/blob/master/packages/tokens/src/global/brandColors.ts)
- [Создание темы Web webLightTheme использует brandWeb.](https://github.com/microsoft/fluentui/blob/master/packages/tokens/src/themes/web/lightTheme.ts)
- [Использование Field Рекомендации по меткам, проверке данных и композиции.](https://fluent2.microsoft.design/components/web/react/core/field/usage)
- [Field API Слоты и контракт для доступных дочерних элементов.](https://github.com/microsoft/fluentui/blob/master/packages/react-components/react-field/library/src/components/Field/Field.types.ts)
- [Использование TabList Рекомендации по компоновке и выбору категории.](https://fluent2.microsoft.design/components/web/react/core/tablist/usage)
- [TabList API Управляемый выбор и параметры управления фокусом.](https://github.com/microsoft/fluentui/blob/master/packages/react-components/react-tabs/library/src/components/TabList/TabList.types.ts)
- [Использование Dialog Рекомендации по размещению формы и модальному взаимодействию.](https://fluent2.microsoft.design/components/web/react/core/dialog/usage)
- [Dialog API Управление видимостью и типы модальных окон.](https://github.com/microsoft/fluentui/blob/master/packages/react-components/react-dialog/library/src/components/Dialog/Dialog.types.ts)
