01 Microsoft Fluent 2 设计系统界面预览 在项目简介和人员之间切换,启用或关闭网站主题,并尝试本地添加无效、重复和成功的内容。不会发送邀请。
基于 Fluent UI React v9 网站令牌以及文档所述 Field、TabList 和 Dialog 结构的独立 HTML/CSS 诠释。并非原生 Fluent 组件运行时。·
实现参考 02
Microsoft Fluent 2 设计系统设计参考 Fluent UI React v9 网站参考。全局数值来自链接的 React 令牌来源。下方颜色值依据 brandWeb 解析浅色别名映射,而非 alias/lightColor.ts 中过时的示例十六进制注释。深色和 Teams 主题使用单独的映射。网站字体排印表目前将 Subtitle1 列为 20/26;React fonts.ts 源文件采用 20/28,本实现遵循该源文件。来源核查日期:2026 年 10 月 3 日;请以 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两种焦点颜色;确保组件边缘处的对比度。
名称 / 规则 值 / 配置 用途 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根据布局方向选择轴;共用间距步长。
名称 / 规则 值 / 配置 用途 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 中常见的紫色并非 Web 默认的蓝色色阶。Windows、iOS 和 Android 原生资源也不能与 React 包互换。
下面的完整示例呈现一份项目简报和本地访问权限列表。它结合了用于构建层级的 Title3 和 Body1、用于切换两个视图的 TabList 和 Tab,以及用于决定访问权限的 Dialog、Field、Input 和 Select。代码并未声称可以邀请真实用户。这样的区分既能演示组件行为,也不会凭空添加共享服务。
阅读配套规范
Provider 实际提供的内容 FluentProvider 会将主题变量应用到其子树中。使用 tokens.colorNeutralBackground2 设置样式的自定义表面会随所选主题变化,而硬编码为白色的表面则不会。请将 Provider 放在需要使用主题的组件上层。该库通过自己的样式运行时为组件设置样式;本示例不会要求你导入一个虚构的 fluent.css 文件。
阅读配套规范
Microsoft Fluent 2 设计系统:语义颜色与真实的 Web 品牌色阶 根据所选品牌色解析别名 浅色别名源将 colorBrandBackground 指向 brand[80]。Web 主题提供 brandWeb,其中第 80 级为 #0f6cbd。因此,在此示例中,#0f6cbd 是解析后的主填充色。别名文件包含较早示例中的十六进制注释;若将这些注释当作当前 Web 调色板,就会得出错误结果。决定该值的是源关系,而不是注释。
简报使用 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,而 Web 设计图表将 Subtitle1 列为 20/26。本示例采用运行时中的这组数值。遇到此类差异时,应核对版本和实现情况,而不是默默混用最适合模型图的数值。
阅读配套规范
文本预设与字体栈 除了视觉预设,也要使用语义化的 HTML:Title3 as="h1" 才是标题;仅设置样式不会形成文档大纲。基础字体栈以 Segoe UI 开头,并包含平台回退字体。不要仅为匹配截图而打包专有字体。本示例使用文档说明的字体栈,因此具体字形取决于查看设备上安装的字体。
自定义文字时,typographyStyles.body1 会组合字体系列、字号、字重和行高。相比只复制 fontSizeBase300,优先使用这组完整样式。访问列表中的电子邮件地址清晰易读,并支持换行;权限标签通过半粗体强调,但不会与页面标题争夺视觉焦点。
阅读配套规范
Microsoft Fluent 2 设计系统:工作区中的间距、形状与密度 React 的间距名称并非设计工具中的名称 Fluent 通用布局指南使用全局尺寸名称和以 4 为单位的节奏,其中包括用于对齐微调的 2、6 和 10。React 提供按轴区分的别名:spacingHorizontalS 为 8px,M 为 12px,L 为 16px,XXL 为 24px;垂直方向也有对应名称。不要将 size120 转换为 spacingHorizontal120;React 中不存在此标识符。
此布局使用 24px 面板内边距、表单字段之间 16px 的间隔,以及访问权限行之间 12px 的间距。确切的页面宽度和双栏布局由应用自行决定,并非官方 Fluent 断点。在窄屏上,团队辅助信息会移到简报下方,而两个标签名称仍保持可见。
阅读配套规范
形状体现组件尺寸 实现说明 可用的圆角令牌包括 4px 的 Medium 和 8px 的 XLarge,但这并不意味着所有控件都应使用相同的圆角。HTML 示例将 4px 控件形状和 8px 文档表面形状作为明确的研究选择。除非所选设计要求确有需要,否则原生组件应保留其受支持的默认结构。不要通过修改生成的类名来强制改变形状。
阅读配套规范
Microsoft Fluent 2 设计系统 TabList:选择状态、面板与键盘行为 选中值属于应用状态 示例在 React 状态中管理标签,并将其传递给 selectedValue。onTabSelect 会接收 data.value,并据此更新该状态。每个 Tab 都有稳定的 value,并通过 aria-controls 指向其面板。每个面板都设置了 role="tabpanel"、aria-labelledby 和隐藏条件。组件不会提供项目简报或访问列表;这些内容由示例明确渲染。
API 将聚焦与选择分开:selectTabOnFocus 默认值为 false。在独立示例中,按 Left/Right 和 Home/End 可移动焦点,按 Enter 或 Space 可激活当前聚焦的选项卡。这种手动激活方式让用户可以查看选项卡,而不会意外切换内容。选项卡标签应保持简短;官方布局指南指出,水平选项卡不会自动换行或滚动。
阅读配套规范
类别增多时,应改用其他模式 Brief 和 People 是同一工作区中两个密切相关的视图,因此适合使用选项卡。组织中的所有项目列表属于导航,而不是另一组选项卡。如果类别在窄屏上无法容纳,应有意设计溢出处理或采用其他选择模式;把标签缩小到难以辨认并不是响应式策略。
阅读配套规范
Microsoft Fluent 2 设计系统指南:无障碍标签与有效验证 字段是一个组合,而不只是经过样式处理的输入框 Field 由标签插槽、子控件、可选提示和验证消息组成。其记录的控件契约包括 id、aria-labelledby、aria-describedby、aria-invalid 和 aria-required。Fluent 输入控件可以直接作为子元素;自定义控件则可以使用 render 函数的属性。这就是为什么用 Field 包裹电子邮件 Input,比在输入框上方放置无关文本更有用。
在示例中,只有在本地提交无效后,validationState 才会设为 error。应用会检查值是否符合电子邮件格式,以及该地址是否已存在于内存列表中。它会保留已输入的值,并通过 validationMessage 提供修改提示。Field 负责呈现并关联验证结果;它不会判断地址是否真实存在、发送邀请或授予访问权限。
阅读配套规范
将验证状态与权限区分开 权限 Select 包含 Can view 和 Can edit 两个选项,而电子邮件字段有自己的错误状态。不要因为一个地址尚未填写完整,就让整个对话框变红。required 是无障碍提示,不能替代服务器端的授权或验证。示例表单使用 noValidate,以便一致地展示显式验证消息;生产服务必须再次验证请求。
地址为空或格式错误:保持对话框打开,并说明可接受的格式。 地址已存在于本地访问列表中:显示针对重复地址的提示,不要再添加一行。 地址有效且尚未添加:在本地添加所选权限,关闭对话框,并如实说明结果仅在本地生效。 阅读配套规范
Microsoft Fluent 2 设计系统对话框:在受支持的结构中构建表单 将提交按钮放在表单中 该组合由 Dialog、DialogTrigger、DialogSurface、form 元素,以及包含 DialogTitle、DialogContent 和 DialogActions 的 DialogBody 构成。form 包裹 body,使页脚中的提交按钮也能参与表单提交。取消按钮的类型为 type="button",并位于用于关闭对话框的 DialogTrigger 中;否则,默认按钮类型可能会意外提交表单。
open 和 onOpenChange 用于控制显示状态。disableButtonEnhancement 可避免对已具备按钮行为的 Fluent Button 再次进行冗余增强。成功处理程序仅在本地验证通过后关闭对话框。验证失败时,已输入的电子邮件和所选权限会保留,供用户修改。
阅读配套规范
根据实际决策选择模态行为 这个简短的访问权限决策使用默认的模态类型。非模态对话框允许用户继续处理背景内容;警报对话框则用于紧急决策,且关闭行为限制更严格。普通的访问权限表单不需要警报语义。官方指南要求将焦点置于对话框内,并在关闭后将焦点返回触发器;独立实现使用原生 dialog 元素,并显式将焦点还给触发器。
阅读配套规范
Microsoft Fluent 2 设计系统自定义样式:不破坏主题 使用 makeStyles 和语义变量 完整代码使用 makeStyles,并通过设计令牌设置界面表面、间距和圆角。这能让自定义面板与 provider 保持一致。生成的类名属于实现细节,不要从 DevTools 中复制到样式表。试验主题时,应更改 provider 主题,并一并检查原生控件和自定义区域。
已发布的时长阶梯范围为 50ms 至 500ms。HTML 示例使用 150ms 的 durationFast 处理简单的悬停变化,并在 prefers-reduced-motion 生效时移除这些过渡效果。这是应用对现有令牌的选择,并不表示每个 Fluent 交互都必须持续 150ms。即使移除动效,内容和错误消息仍应清晰易懂。
阅读配套规范
测试实现方式的适用边界 实现说明 嵌入示例明确标注为 HTML/CSS,而不是原生 Fluent 实现。它展示已验证的值和访问权限对话框交互,且不会在预览中下载框架。评估原生样式、portal、焦点管理和组件更新时,请使用完整的 React 示例。独立交互检查通过,只能说明此示例符合要求,并不代表生产级共享应用已通过认证。
阅读配套规范
Microsoft Fluent 2 设计系统安装并验证完整组合 提供可运行入口,而不是任务提纲 此设置面向已有的 React 应用,该应用需包含 id 为 root 的元素。安装 v9 套件,然后将 JSX 入口放入应用。导入语句、provider、组件树、处理程序、状态和根节点渲染代码均已一并提供。不会调用外部共享 API。如果项目模板已经渲染 App,请保留现有根入口,并导出 App,而不是再创建一个根节点。
在 lockfile 中记录解析后的软件包版本。链接的源代码参考资料描述的是经检查的 React v9 实现,而 master URL 可能随时间变化。升级前,请比较导出的主题,以及此处使用的 Field、TabList 和 Dialog 的具体行为。此设置不会混用 v8 @fluentui/react 组件与 v9 示例。
在现有的 React 项目中安装 @fluentui/react-components@9,并保留生成的锁文件。 在一个 FluentProvider 下渲染提供的入口,并以 webLightTheme 作为初始主题。 打开对话框,提交无效地址,修正地址,并确认新增人员仅出现在本地状态中。 在 webDarkTheme 下重复测试并使用键盘导航;验证面板关联、错误播报和焦点返回。 阅读配套规范
真实共享产品需要接入的内容 只有在定义好服务契约后,才能用服务调用替代本地添加:契约需明确已验证身份的操作者、目标工作区、允许的权限值、邀请结果和重复邀请的处理方式。如果服务拒绝请求,应保持对话框打开并显示已确认的结果,而不是乐观地声称对方已获得访问权限。这些规则不属于 Fluent 的范畴,但代码明确标出了交接点,而不是把它藏在通用的提交按钮后面。
阅读配套规范
Microsoft Fluent 2 设计系统组件示例 完整的独立交互示例 将完整文档保存为 index.html,并在浏览器中打开。这是对 Fluent UI React v9 Web 令牌以及已记录的 Field、TabList 和 Dialog 结构的独立 HTML/CSS 呈现,并非原生 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 控件;运行单独的 React v9 入口,以评估 FluentProvider、Field 和 Dialog 的行为。 完整的原生实现入口 此处汇总了从设置到完整实现的内容,并附有重点说明 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 应用的完整 React v9 组件组合。独立预览是单独的 HTML/CSS 呈现。此代码仅执行本地验证和状态更新;不会发送电子邮件,也不会实际更改权限。源 API 已核对;在目标项目中测试之前,不保证能通过原生包编译。 Field + Input + Select:带标签的访问权限表单 以下代码片段使用下方完整配置中的状态变量和导入项。Field 负责标签、提示和验证状态的呈现;电子邮件规则和重复项检查由应用负责。
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 都应使用不同的 value。 selectTabOnFocus 默认为 false。方向键移动焦点和激活选项是分开的,除非有意更改此行为。 Fluent 水平选项卡不会自动换行或滚动;示例使用两个较短的标签,以确保它们始终可见。 04
Microsoft Fluent 2 设计系统 Agent 配置 实现起点 适用于现有 React 应用的完整 React v9 组件组合。独立预览是单独的 HTML/CSS 呈现。此代码仅执行本地验证和状态更新;不会发送电子邮件,也不会实际更改权限。源 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 />);
适用于你项目的提示词 Agent 提示词 复制提示词
通过 @fluentui/react-components 使用 Fluent UI React v9。使用 webLightTheme 渲染 FluentProvider,并提供 webDarkTheme 切换功能。使用 Title3 和 Body1 组合简介;使用受控的 TabList 和选项卡视图,并显式关联面板;创建包含 Field/Input/Select 和真实表单的访问对话框。自定义界面表面和间距时使用令牌。遵循提供的组件示例和运行时字号配对,包括 20/28 的 Subtitle1。保留无效输入、拒绝本地重复地址、恢复对话框焦点,并如实说明本地添加的实际情况。只有在获得授权和结果契约后,才接入共享服务。 05
Microsoft Fluent 2 设计系统的来源与范围 这是对 Fluent UI React v9 的 OpenDesign 应用研究,并非 Microsoft 产品或在线共享服务。独立预览是对已核实的 Web 令牌和组件结构的 HTML/CSS 呈现;下方的 React 代码使用原生 Fluent 组件。参考值基于 2026 年 10 月 3 日核对的关联源快照,并不保证每个版本、平台或品牌都使用完全相同的值。