docs(ui): expand bilingual component guides

This commit is contained in:
Maofeng
2026-09-21 17:27:45 +08:00
parent fb985edb20
commit 4c61e79d0d
22 changed files with 1636 additions and 192 deletions
@@ -0,0 +1,64 @@
---
title: 锚定式个人菜单
description: 使用 Popover 的连续尾巴、滚动视口和 Base UI render 组合个人菜单。
order: 31
toc:
- id: example
title: 示例
- id: why-these-props
title: 参数说明
- id: accessibility
title: 可访问性
---
## 示例 {#example}
```tsx
import { Avatar, AvatarFallback } from "@workspace/ui/components/avatar"
import { Button } from "@workspace/ui/components/button"
import { SettingsIcon } from "@workspace/ui/components/icon"
import {
Popover,
PopoverContent,
PopoverDescription,
PopoverTitle,
PopoverTrigger,
} from "@workspace/ui/components/popover"
export function AccountMenu() {
return (
<Popover>
<PopoverTrigger
render={<Button variant="ghost" size="icon" aria-label="账户菜单" />}
>
<Avatar>
<AvatarFallback>JF</AvatarFallback>
</Avatar>
</PopoverTrigger>
<PopoverContent
align="end"
side="bottom"
showArrow={{ cornerRadius: "2xl", tailSize: "sm" }}
className="w-80 gap-0 p-0"
viewportClassName="max-h-[min(28rem,var(--available-height))] p-4"
>
<PopoverTitle>Jaydon Frankie</PopoverTitle>
<PopoverDescription>demo@minimals.cc</PopoverDescription>
<Button variant="ghost" className="mt-4 w-full justify-start">
<SettingsIcon />
界面外观
</Button>
</PopoverContent>
</Popover>
)
}
```
## 参数说明 {#why-these-props}
`align="end"` 让面板尾部与触发器尾部对齐,逻辑方向会自动适配 RTL。`showArrow` 启用本地连续尾巴。外层 `className` 控制整个表面,内部 `viewportClassName` 控制滚动区域;这种分离能让长菜单滚动而不裁剪尾巴。
## 可访问性 {#accessibility}
触发按钮使用图标或头像时必须提供 `aria-label`。面板使用 `PopoverTitle` 与 `PopoverDescription`,关闭与焦点返回交给 Base UI Root 管理。若内容本质上是一组选项或命令,应改用 DropdownMenu 或 Command,而不是手工添加错误的 menu role。
@@ -1,37 +1,96 @@
---
title: 组合表单
description: 组合 Field、Input 和 Button,同时让领域状态继续由应用管理。
title: 组合资料表单
description: 用 Field、Input、Select 和 Button 组合可访问表单,同时把状态留在应用层。
order: 30
toc:
- id: create-the-form
title: 创建表单
- id: complete-example
title: 完整示例
- id: validation
title: 表单验证
title: 验证状态
- id: controlled-select
title: 受控 Select
- id: styling
title: 样式
title: 样式覆盖
---
## 创建表单 {#create-the-form}
## 完整示例 {#complete-example}
```tsx
import * as React from "react"
import { Button } from "@workspace/ui/components/button"
import { Field, FieldError, FieldLabel } from "@workspace/ui/components/field"
import {
Field,
FieldDescription,
FieldError,
FieldGroup,
FieldLabel,
} from "@workspace/ui/components/field"
import { Input } from "@workspace/ui/components/input"
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@workspace/ui/components/select"
;<form onSubmit={saveProfile}>
<Field>
<FieldLabel htmlFor="display-name">显示名称</FieldLabel>
<Input id="display-name" name="displayName" />
<FieldError>{errors.displayName}</FieldError>
</Field>
<Button type="submit">保存资料</Button>
</form>
export function ProfileForm() {
const [role, setRole] = React.useState("editor")
const [error, setError] = React.useState("")
function submit(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault()
const data = new FormData(event.currentTarget)
setError(data.get("displayName") ? "" : "请输入显示名称")
}
return (
<form onSubmit={submit} className="max-w-md space-y-6">
<FieldGroup>
<Field data-invalid={Boolean(error)}>
<FieldLabel htmlFor="display-name">显示名称</FieldLabel>
<Input
id="display-name"
name="displayName"
aria-invalid={Boolean(error)}
aria-describedby="display-name-error"
/>
<FieldDescription>其他成员会看到这个名称。</FieldDescription>
<FieldError id="display-name-error">{error}</FieldError>
</Field>
<Field>
<FieldLabel htmlFor="role">角色</FieldLabel>
<Select
value={role}
onValueChange={(value) => setRole(value ?? "editor")}
>
<SelectTrigger id="role">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="viewer">只读</SelectItem>
<SelectItem value="editor">编辑者</SelectItem>
<SelectItem value="admin">管理员</SelectItem>
</SelectContent>
</Select>
</Field>
</FieldGroup>
<Button type="submit">保存资料</Button>
</form>
)
}
```
## 表单验证 {#validation}
## 验证状态 {#validation}
UI 包负责渲染验证状态,但不会指定表单库或 Schema。请在应用边界连接原生表单数据、React 状态或表单框架。
UI 包不选择验证库。应用负责产生错误,再把同一状态连接到 `Field data-invalid`、输入控件的 `aria-invalid` 和 `FieldError`。多个 Schema 错误可通过 `FieldError errors={errors}` 传入,它会去重后渲染列表。
## 样式 {#styling}
## 受控 Select {#controlled-select}
受支持的语义变化使用组件变体,局部布局使用 `className`。需要合并条件类名或消费者覆盖时,使用 `@workspace/ui/lib/utils` 中的 `cn`。
本地 Select 基于 Base UI,使用 `value` 与 `onValueChange`。`SelectTrigger` 的箭头由组件自己渲染,只需放入 `SelectValue`;不要再添加第二个 Chevron。弹层默认与 trigger 对齐并使用其宽度,可通过 `SelectContent` 的 `side`、`align`、`sideOffset` 与 `alignItemWithTrigger` 调整。
## 样式覆盖 {#styling}
先使用组件已有的 `variant`、`size` 和结构部件,再用 `className` 处理页面布局。组件内部使用 `cn` 合并 class,因此消费端的 Tailwind class 可以覆盖默认 utility。跨多个页面重复的覆盖应沉淀为新 variant 或复合组件,而不是复制长 class 字符串。
@@ -0,0 +1,58 @@
---
title: 消息线程
description: 组合滚动容器、消息布局与气泡,同时保护用户阅读历史消息的位置。
order: 32
toc:
- id: structure
title: 组件结构
- id: messages
title: 渲染消息
- id: ownership
title: 状态归属
---
## 组件结构 {#structure}
```tsx
import {
MessageScroller,
MessageScrollerButton,
MessageScrollerContent,
MessageScrollerItem,
MessageScrollerProvider,
MessageScrollerViewport,
} from "@workspace/ui/components/message-scroller"
export function Thread({ messages }: { messages: ChatMessage[] }) {
return (
<MessageScrollerProvider>
<MessageScroller className="h-[32rem]">
<MessageScrollerViewport>
<MessageScrollerContent className="gap-5 p-4">
{messages.map((message, index) => (
<MessageScrollerItem
key={message.id}
messageId={message.id}
scrollAnchor={index === messages.length - 1}
>
<ChatMessageRow message={message} />
</MessageScrollerItem>
))}
</MessageScrollerContent>
</MessageScrollerViewport>
<MessageScrollerButton />
</MessageScroller>
</MessageScrollerProvider>
)
}
```
## 渲染消息 {#messages}
在 `ChatMessageRow` 中使用 `Message`、`MessageAvatar`、`MessageContent`、`MessageHeader` 和 `MessageFooter` 表达参与者结构,再在 Content 内组合一个或多个 `Bubble`。发送者消息可使用 `align="end"`,但不要根据文本语言交换消息归属。
附件放入相同的 `MessageContent`。如果一条消息没有气泡背景,使用 `Bubble variant="ghost"`,Header 与 Footer 会自动移除气泡对应的水平 padding。
## 状态归属 {#ownership}
`MessageScroller` 管理跟随底部、可见性与跳转按钮;应用管理消息数组、分页、加载、已读和重试。加载更早消息时应保持稳定 ID,并使用 scroller API 维持视觉锚点,而不是在组件内复制一套滚动监听。