docs(ui): expand bilingual component guides
This commit is contained in:
@@ -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 维持视觉锚点,而不是在组件内复制一套滚动监听。
|
||||
Reference in New Issue
Block a user