Кроссплатформенный продукт · Новый ресурс

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

Создайте рабочую область Fluent UI React v9 с настоящими семантическими токенами, веб-шкалами типографики и отступов, управляемыми вкладками и диалогом доступа с валидацией. Сравните значения тем, изучите анатомию компонентов и проверьте локальное взаимодействие перед использованием полного примера React.

Руководство OpenDesign9 глав64 справочные записиАвторское руководство по приложению
01

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

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

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

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 / spacingVerticalNone0Выбирайте ось в соответствии с направлением компоновки; общий шаг интервалов.
spacingHorizontalXXS / spacingVerticalXXS2pxВыбирайте ось в соответствии с направлением компоновки; общий шаг интервалов.
spacingHorizontalXS / spacingVerticalXS4pxВыбирайте ось в соответствии с направлением компоновки; общий шаг интервалов.
spacingHorizontalSNudge / spacingVerticalSNudge6pxВыбирайте ось в соответствии с направлением компоновки; небольшая поправка для выравнивания значка.
spacingHorizontalS / spacingVerticalS8pxВыбирайте ось в соответствии с направлением компоновки; общий шаг интервалов.
spacingHorizontalMNudge / spacingVerticalMNudge10pxВыбирайте ось в соответствии с направлением компоновки; небольшая поправка для выравнивания значка.
spacingHorizontalM / spacingVerticalM12pxВыбирайте ось в соответствии с направлением компоновки; общий шаг интервалов.
spacingHorizontalL / spacingVerticalL16pxВыбирайте ось в соответствии с направлением компоновки; общий шаг интервалов.
spacingHorizontalXL / spacingVerticalXL20pxВыбирайте ось в соответствии с направлением компоновки; общий шаг интервалов.
spacingHorizontalXXL / spacingVerticalXXL24pxВыбирайте ось в соответствии с направлением компоновки; общий шаг интервалов.
spacingHorizontalXXXL / spacingVerticalXXXL32pxВыбирайте ось в соответствии с направлением компоновки; общий шаг интервалов.

Комбинации веб-типографики React

Источник
Название / правилоЗначение / конфигурацияПрименение
Caption2 · fontSizeBase10010px / 14px / 400Размер / высота строки / насыщенность. Используйте вместе с lineHeightBase100.
Caption1 · fontSizeBase20012px / 16px / 400Размер / межстрочный интервал / насыщенность. Используйте вместе с lineHeightBase200.
Body1 · fontSizeBase30014px / 20px / 400Размер / межстрочный интервал / насыщенность. Используйте вместе с lineHeightBase300.
Body2 / Subtitle2 · fontSizeBase40016px / 22px / 400 / 600Размер / межстрочный интервал / насыщенность. Используйте вместе с lineHeightBase400.
Subtitle1 · fontSizeBase50020px / 28px / 600Размер / межстрочный интервал / насыщенность. Используйте вместе с lineHeightBase500.
Title3 · fontSizeBase60024px / 32px / 600Размер / межстрочный интервал / насыщенность. Используйте вместе с lineHeightBase600.
Title2 · fontSizeHero70028px / 36px / 600Размер / межстрочный интервал / насыщенность. Используйте вместе с lineHeightHero700.
Title1 · fontSizeHero80032px / 40px / 600Размер / межстрочный интервал / насыщенность. Используйте вместе с lineHeightHero800.
LargeTitle · fontSizeHero90040px / 52px / 600Размер / межстрочный интервал / насыщенность. Используйте вместе с lineHeightHero900.
Display · fontSizeHero100068px / 92px / 600Размер / межстрочный интервал / насыщенность. Используйте вместе с lineHeightHero1000.

Семейство шрифтов и насыщенность

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

Токены формы

Источник
Название / правилоЗначение / конфигурацияПрименение
borderRadiusNone0Сначала используйте стандартный вариант компонента: этот токен доступен, но не обязателен для каждой поверхности.
borderRadiusSmall2pxСначала используйте стандартный вариант компонента: этот токен доступен, но не обязателен для каждой поверхности.
borderRadiusMedium4pxСначала используйте стандартный вариант компонента: этот токен доступен, но не обязателен для каждой поверхности.
borderRadiusLarge6pxСначала используйте стандартный вариант компонента: этот токен доступен, но не обязателен для каждой поверхности.
borderRadiusXLarge8pxСначала используйте стандартный вариант компонента: этот токен доступен, но не обязателен для каждой поверхности.
borderRadius2XLarge12pxСначала используйте стандартный вариант компонента: этот токен доступен, но не обязателен для каждой поверхности.
borderRadius3XLarge16pxСначала используйте стандартный вариант компонента: этот токен доступен, но не обязателен для каждой поверхности.
borderRadius4XLarge24pxСначала используйте стандартный вариант компонента: этот токен доступен, но не обязателен для каждой поверхности.
borderRadius5XLarge32pxСначала используйте стандартный вариант компонента: этот токен доступен, но не обязателен для каждой поверхности.
borderRadius6XLarge40pxСначала используйте стандартный вариант компонента: этот токен доступен, но не обязателен для каждой поверхности.
borderRadiusCircular10000pxСначала используйте стандартный вариант компонента: этот токен доступен, но не обязателен для каждой поверхности.

Шкала длительности анимации

Источник
Название / правилоЗначение / конфигурацияПрименение
durationUltraFast50msДоступный токен среды выполнения; в примере для наведения используется Fast, а при включённом режиме уменьшения анимации переходы отключаются.
durationFaster100msДоступный токен среды выполнения; в примере для наведения используется Fast, а при включённом режиме уменьшения анимации переходы отключаются.
durationFast150msДоступный токен среды выполнения; в примере для наведения используется Fast, а при включённом режиме уменьшения анимации переходы отключаются.
durationNormal200msДоступный токен среды выполнения; в примере для наведения используется Fast, а при включённом режиме уменьшения анимации переходы отключаются.
durationGentle250msДоступный токен среды выполнения; в примере для наведения используется Fast, а при включённом режиме уменьшения анимации переходы отключаются.
durationSlow300msДоступный токен среды выполнения; в примере для наведения используется Fast, а при включённом режиме уменьшения анимации переходы отключаются.
durationSlower400msДоступный токен среды выполнения; в примере для наведения используется Fast, а при включённом режиме уменьшения анимации переходы отключаются.
durationUltraSlow500msДоступный токен среды выполнения; в примере для наведения используется Fast, а при включённом режиме уменьшения анимации переходы отключаются.

Смена темы — это изменение семантики, а не инверсия цветов

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

Полное руководство по дизайн-системе Microsoft Fluent 2

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

DESIGN-microsoft-fluent.md

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

Скачать руководство

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

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

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

Из этого руководства OpenDesign

Пример ниже целиком отображает описание проекта и локальный список пользователей с доступом. Для создания иерархии он сочетает 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. На узком экране дополнительный контекст команды перемещается под описание проекта, а обе метки вкладок остаются видимыми.

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

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

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

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. Содержимое и сообщения об ошибках остаются понятными и без анимации.

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

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

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

Дизайн-система 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.

html
<!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">[email protected]</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">[email protected]</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, [email protected]</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(['[email protected]','[email protected]']);
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 [email protected]');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. Следуйте документированным требованиям к пакетам и сборке из раздела настройки.

jsx
// 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: '[email protected]', 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 [email protected]'); 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 и поиск дубликатов.

jsx
<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.

jsx
<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 не переносятся на новую строку и не прокручиваются автоматически; в примере используются две короткие метки, которые остаются видимыми.
04

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

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

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

jsx
// 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: '[email protected]', 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 [email protected]'); 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 />);

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

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

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

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