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
+76 -9
View File
@@ -1,31 +1,98 @@
---
title: 添加响应式行为
description: 当 CSS 无法表达行为变化时,使用共享的断点和交互 Hooks。
title: 响应式与交互 Hooks
description: 在行为必须随视口改变时同步断点,并为自定义交互元素添加波纹。
order: 20
toc:
- id: breakpoints
title: 断点
- id: breakpoint-contract
title: 断点约定
- id: reading-breakpoints
title: 读取断点
- id: root-classes
title: 根节点 class
- id: server-rendering
title: 服务端渲染
- id: ripple
title: 波纹交互
---
## 断点 {#breakpoints}
## 断点约定 {#breakpoint-contract}
共享断点把视口分成三段:
| 返回值 | 范围 |
| --------- | --------------- |
| `mobile` | 小于 768px |
| `tablet` | 768px 至 1023px |
| `desktop` | 大于等于 1024px |
仅视觉变化应优先使用 CSS。只有挂载内容、交互方式或数据量必须变化时,才在渲染逻辑中读取断点。
## 读取断点 {#reading-breakpoints}
```tsx
import { useBreakpoint, useIsMobile } from "@workspace/ui/hooks/use-breakpoint"
import {
useBreakpoint,
useIsDesktop,
useIsMobile,
useIsTablet,
useOnBreakpoint,
} from "@workspace/ui/hooks/use-breakpoint"
const breakpoint = useBreakpoint()
const isMobile = useIsMobile()
```
视觉变化优先使用 CSS 响应式变体。只有组件行为或挂载内容必须变化时才使用这些 Hooks。
每个 hook 通过 `useSyncExternalStore` 订阅同一组 `matchMedia` 查询。`useOnBreakpoint(listener)` 只在断点发生变化后调用 listener,不会把初始值作为一次变化上报。
## 根节点 class {#root-classes}
挂载 `BreakpointReactor` 后,文档根节点会且只会拥有 `.ui:mobile`、`.ui:tablet`、`.ui:desktop` 中的一个:
```tsx
import { BreakpointReactor } from "@workspace/ui/hooks/use-breakpoint"
;<>
<BreakpointReactor />
<App />
</>
```
`globals.css` 把它们注册为 `mobile:`、`tablet:`、`desktop:` 自定义变体。这样组件可以让 CSS 与行为层使用同一份三段式定义。
## 服务端渲染 {#server-rendering}
断点 Store 使用确定的桌面端服务端快照。当应用需要 CSS 与行为断点在水合前保持一致时,可用 `getBreakpointInitializationScript` 提前设置匹配的根节点类名。
服务端快照固定为 `desktop`,保证 SSR 可重复。为了在水合前写入真实 class,可把 `getBreakpointInitializationScript()` 的结果作为内联脚本放到文档头部:
```tsx
import { getBreakpointInitializationScript } from "@workspace/ui/hooks/use-breakpoint"
;<script
dangerouslySetInnerHTML={{ __html: getBreakpointInitializationScript() }}
/>
```
在严格 CSP 下为脚本传入应用生成的 nonce。脚本只同步 class,不替代 `BreakpointReactor` 后续监听变化。
## 波纹交互 {#ripple}
`useRipple` 返回一个 ref 回调,为交互元素加入指针反馈。元素应创建定位上下文,使生成的覆盖层使用正确边界。
`Button` 已自动使用 `useRippleRef`,普通按钮无需重复配置。自定义交互表面可直接使用 hook:
```tsx
import { useRipple } from "@workspace/ui/hooks/use-ripple"
export function Tile() {
const ref = useRipple<HTMLButtonElement>({
duration: 500,
opacity: 0.2,
rippleClassName: "bg-primary",
})
return (
<button ref={ref} className="relative">
打开项目
</button>
)
}
```
元素必须建立定位上下文,通常使用 `relative`。hook 会忽略 disabled、`aria-disabled="true"` 和非主鼠标键,并在 ref 变化或卸载时移除监听器和动画节点。需要合并外部 ref 时使用 `useRippleRef(ref, options)`。
@@ -1,19 +1,29 @@
---
title: UI 本地化
description: 只加载当前语言所需的 UI 词典和日历 locale。
description: 将应用翻译器桥接到 UI 组件,并加载当前语言的日历 locale。
order: 21
toc:
- id: catalog-source
title: 词典来源
- id: direct-imports
title: 直接导入
- id: supported-locales
title: 支持的语言
- id: two-layers
title: 两层职责
- id: catalogs
title: 合并词典
- id: bridge-provider
title: 桥接 Provider
- id: fallback
title: 回退行为
- id: direction
title: 文本方向
---
## 词典来源 {#catalog-source}
## 两层职责 {#two-layers}
在应用国际化配置中加入 UI 词典来源:
`@workspace/i18n` 管理应用 locale、词典加载、格式化和切换;`@workspace/ui/components/i18n` 是轻量适配层,让 UI primitives 不绑定某个国际化框架。后者接收一个兼容的翻译函数、语言标签和 `react-day-picker` locale。
这种拆分允许 UI 包独立复用,也允许应用继续使用工作区的完整国际化运行时。
## 合并词典 {#catalogs}
在应用的 `i18n.config.json` 中加入 UI 词典来源:
```json
{
@@ -21,16 +31,50 @@ toc:
}
```
locales 根入口只包含元数据和类型,不会导入所有语言的词典。
根入口 `@workspace/ui/locales` 只导出 `uiCatalogLocales`、`uiMessageIds` 和类型,不会静态导入所有语言。当前仅支持 `en-US` 与 `zh-Hans`。
## 直接导入 {#direct-imports}
## 桥接 Provider {#bridge-provider}
```ts
import { calendarLocale, messages } from "@workspace/ui/locales/zh-Hans"
从当前应用 Provider 中读取翻译器,再传给 UI Provider:
```tsx
import { useLocaleDefinition, useTranslate } from "@workspace/i18n"
import {
I18nProvider as UiI18nProvider,
type CalendarLocale,
} from "@workspace/ui/components/i18n"
import { calendarLocale as enUS } from "@workspace/ui/locales/en-US"
import { calendarLocale as zhHans } from "@workspace/ui/locales/zh-Hans"
const calendarLocales: Record<string, CalendarLocale> = {
"en-US": enUS,
"zh-Hans": zhHans,
}
export function AppUiI18nProvider({ children }: React.PropsWithChildren) {
const locale = useLocaleDefinition()
const t = useTranslate()
return (
<UiI18nProvider
calendarLocale={calendarLocales[locale.locale]}
locale={locale.languageTag ?? locale.locale}
t={t}
>
{children}
</UiI18nProvider>
)
}
```
每个语言入口还会导出对应的 `react-day-picker` locale。
`Calendar` 会优先使用组件自身的 `locale` prop,其次使用 Provider 的 `calendarLocale`;月份格式化与 `lang` 默认使用 Provider 的语言标签。
## 支持的语言 {#supported-locales}
## 回退行为 {#fallback}
UI 词典支持简体中文(`zh-Hans`)和美式英语(`en-US`)。
没有 Provider 时,UI 层使用 `en-US` 和消息描述符中的英文原文。因此独立测试和 Story 不会因为缺少 Provider 崩溃,但正式应用仍应桥接 Provider,保证 aria-label 与可见文本一致。
自定义组件可以使用 `uiMessages`、`useMessage`、`useTranslate`、`LocalizedText`、`useLocale` 和 `useCalendarLocale`。产品业务文案应定义在应用或领域 package 中,不要追加到 UI 词典。
## 文本方向 {#direction}
翻译语言与布局方向是两件事。切换 locale 时,应用应同步设置 `document.documentElement.lang` 与 `dir`,并在需要时使用 `DirectionProvider`。本地组件使用 `start`、`end`、`ps`、`pe` 等逻辑方向 utility,`components.json` 也开启了 `rtl`。
@@ -0,0 +1,67 @@
---
title: 本仓库适配
description: 记录相对 shadcn/ui Base UI 源码必须保留的实现差异与升级步骤。
order: 22
toc:
- id: baseline
title: 上游基线
- id: icons-and-ripple
title: 图标与波纹
- id: localization
title: 本地化
- id: popover
title: Popover 扩展
- id: select
title: Select 修正
- id: upgrade-checklist
title: 升级清单
---
## 上游基线 {#baseline}
本地组件最初来自 shadcn/ui 的 Base UI registry,配置风格为 `base-vega`。上游可参考 [组件文档](https://ui.shadcn.com/docs/components/base)和 [GitHub 源码目录](https://github.com/shadcn-ui/ui/tree/main/apps/v4/registry/bases/base/ui),但上游当前版本不等于本仓库锁定的实现。
判断行为时按以下优先级:本地源码、本文档、本地 `components.json`,最后才是上游最新示例。
## 图标与波纹 {#icons-and-ripple}
`components/icon.tsx` 把 Lucide 图标名映射到 `@icones/react` 组件,并保留一批与上游同名的导出。已有组件已改为从该入口导入。CLI 新增或更新的源码如果出现 `lucide-react`,应转换到这个入口。
`Button` 合并传入 ref 与 `useRippleRef`。修改 Button 时必须保留 `relative` 定位、波纹 ref 合并和 disabled 行为。
## 本地化 {#localization}
Breadcrumb、Calendar、Carousel、Dialog、Pagination、Sheet、Sidebar、Spinner 与 Toast 等组件已移除部分硬编码英文,改用 `components/i18n.tsx` 中的消息描述符。更新这些文件时要检查 aria-label、sr-only 文本和默认说明,不能只比较可见 UI。
词典对外只发布 `en-US` 和 `zh-Hans`。新增消息时要同步:
1. `uiMessages` 描述符。
2. `uiMessageIds` 类型列表。
3. 两个 locale 的 `messages`。
4. 使用该消息的组件。
## Popover 扩展 {#popover}
本地 `PopoverContent` 比上游包装层多出:
- `anchor` 与 `positionMethod` 转发。
- `showArrow`,支持 `AnchoredSurface` 配置对象。
- `viewportClassName`,把可滚动内容与必须 overflow-visible 的外层表面分开。
- 按锚点尺寸计算的默认 side offset 与连续曲线尾巴。
- `createPopoverHandle` 与 `PopoverHandle` 导出。
不要用上游文件整体替换它。特别要测试长内容滚动、四个 side、碰撞翻转、窄触发器和 RTL。
## Select 修正 {#select}
`SelectTrigger` 把 `ChevronDownIcon` 作为 `SelectPrimitive.Icon` 的 child 渲染,而不是把图标组件交给 `render`。这是为当前 Base UI 与 Icones 组合保留的修正。升级 Select 时要确认 trigger 只出现一个图标,并验证 SSR 与 hydration。
## 升级清单 {#upgrade-checklist}
1. 先查看 shadcn 文档与对应 registry 源码的提交差异,不直接执行覆盖。
2. 通过 CLI 生成到临时分支或逐文件暂存,保留本地 public exports。
3. 将直接图标依赖迁回 `components/icon`。
4. 恢复翻译消息、逻辑方向 utility 与本地 theme class。
5. 对 Button、Popover、Select、Calendar、Toast 和 Sidebar 做重点回归。
6. 运行 UI typecheck、全仓 lint/format,再构建实际消费应用。
7. 在 light、dark、LTR、RTL、mobile、tablet、desktop 下手动检查交互。
@@ -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 维持视觉锚点,而不是在组件内复制一套滚动监听。
+85 -14
View File
@@ -1,30 +1,101 @@
---
title: 选择组件
description: 按职责选择基础组件,而不是导入一个庞大的统一组件入口。
title: 组件目录
description: 按职责查找组件入口,并理解本地 Base UI 的组合方式。
order: 11
toc:
- id: forms
title: 表单与输入
- id: overlays
title: 浮层与菜单
- id: content
- id: composition-model
title: 组合模型
- id: actions-and-forms
title: 操作与表单
- id: overlays-and-navigation
title: 浮层与导航
- id: content-and-feedback
title: 内容与反馈
- id: layout-and-data
title: 布局与数据
- id: icons
title: 图标
---
## 表单与输入 {#forms}
## 组合模型 {#composition-model}
普通表单可使用 `Button`、`Input`、`Textarea`、`Field` 和 `Label`。选择类输入可加入 `Checkbox`、`RadioGroup`、`Switch` 或 `Slider`;更丰富的选择场景可使用 `Select`、`Combobox`、`Command` 或 `Calendar`。
组件采用 shadcn/ui 的“复制源码并组合 primitives”模型,但本仓库选择的是 Base UI 实现。组件通常由 Root、Trigger、Content、Item 等部分组成,状态和可访问性由底层 primitive 管理,外观通过 Tailwind class 与 `data-slot` 暴露。
## 浮层与菜单 {#overlays}
Base UI 使用 `render` 组合自定义元素,而不是 Radix 版本常见的 `asChild`:
`Dialog`、`AlertDialog`、`Sheet` 和 `Drawer` 用于模态界面。`Popover`、`HoverCard` 和 `Tooltip` 用于锚定式信息展示,菜单模块则覆盖上下文菜单、下拉菜单、菜单栏和导航模式。
```tsx
import { Button } from "@workspace/ui/components/button"
import { Dialog, DialogTrigger } from "@workspace/ui/components/dialog"
## 内容与反馈 {#content}
;<Dialog>
<DialogTrigger render={<Button variant="outline" />}>编辑资料</DialogTrigger>
</Dialog>
```
卡片、列表项、表格、图表、进度条、骨架屏、加载器、空状态、消息和 Toast 构成通用展示词汇。Accordion、Collapsible、Tabs、Carousel 和 Pagination 用于组织更大规模的内容。
只在需要替换底层元素时使用 `render`。普通内容直接作为 children 传入。
## 操作与表单 {#actions-and-forms}
| 目标 | 组件入口 |
| -------------- | -------------------------------------------------- |
| 按钮与成组操作 | `button`、`button-group`、`toggle`、`toggle-group` |
| 文本输入 | `input`、`textarea`、`input-group`、`input-otp` |
| 字段结构 | `field`、`label` |
| 布尔与单选 | `checkbox`、`switch`、`radio-group` |
| 范围与进度 | `slider`、`progress` |
| 选项与命令 | `native-select`、`select`、`combobox`、`command` |
| 日期 | `calendar` |
简单、移动端优先且不需要自定义浮层的选项优先用 `NativeSelect`。需要搜索、多选或自定义项时,分别选择 `Combobox`、`Select` 或 `Command`。
## 浮层与导航 {#overlays-and-navigation}
| 目标 | 组件入口 |
| -------------- | -------------------------------------------------------- |
| 模态确认与编辑 | `alert-dialog`、`dialog`、`sheet`、`drawer` |
| 锚定内容 | `popover`、`hover-card`、`tooltip`、`anchored-surface` |
| 菜单 | `dropdown-menu`、`context-menu`、`menubar` |
| 页面导航 | `breadcrumb`、`navigation-menu`、`pagination`、`sidebar` |
Dialog、Sheet 与 Drawer 都提供受控状态和组合部件。重要破坏性操作使用 `AlertDialog`,短暂说明使用 `Tooltip`,需要交互控件的锚定面板使用 `Popover`。
## 内容与反馈 {#content-and-feedback}
| 目标 | 组件入口 |
| ---------- | ----------------------------------------------------- |
| 容器与状态 | `card`、`item`、`empty`、`alert`、`badge`、`marker` |
| 加载与通知 | `skeleton`、`spinner`、`toast` |
| 消息界面 | `message`、`bubble`、`attachment`、`message-scroller` |
| 分段内容 | `accordion`、`collapsible`、`tabs`、`carousel` |
`Message` 负责参与者、对齐和元数据;`Bubble` 负责消息正文表面;`Attachment` 表达上传生命周期;`MessageScroller` 管理自动跟随和回到底部按钮。它们都是展示原语,不拥有会话数据。
## 布局与数据 {#layout-and-data}
| 目标 | 组件入口 |
| -------- | ------------------------------------------------------- |
| 数据展示 | `table`、`chart` |
| 布局 | `separator`、`aspect-ratio`、`resizable`、`scroll-area` |
| 方向 | `direction` |
| 键盘提示 | `kbd` |
| 头像 | `avatar` |
图表模块封装 Recharts 的容器、Tooltip、Legend 与样式映射,不替应用决定数据模型。`DirectionProvider` 直接转发 Base UI 的方向上下文;页面仍应同步设置 HTML 的 `dir` 属性。
## 图标 {#icons}
从 `@workspace/ui/components/icon` 导入具名图标。该模块统一了 `@icones/react` 的图标词汇,使应用和各个包使用同一图标来源。
从本地 Icones 适配层导入具名图标:
```tsx
import { SearchIcon, SettingsIcon } from "@workspace/ui/components/icon"
```
组件会为没有显式尺寸 class 的 SVG 提供默认尺寸。图标按钮仍要提供可访问名称:
```tsx
<Button size="icon" aria-label="搜索">
<SearchIcon aria-hidden="true" />
</Button>
```
不要在新组件中重新引入 `lucide-react`;需要新增图标时,在 `components/icon.tsx` 中用 `createIcon` 统一注册。
@@ -0,0 +1,95 @@
---
title: 反馈与消息
description: 配置 Toast,并用 Message、Bubble、Attachment 和 MessageScroller 组合消息界面。
order: 13
toc:
- id: toast-provider
title: Toast Provider
- id: send-toast
title: 发送 Toast
- id: message-composition
title: 消息组合
- id: attachments
title: 附件状态
- id: scrolling
title: 消息滚动
---
## Toast Provider {#toast-provider}
在应用根节点附近挂载一次 `Toaster`:
```tsx
import { Toaster } from "@workspace/ui/components/toast"
export function App() {
return (
<>
<Routes />
<Toaster />
</>
)
}
```
默认 viewport 固定在小屏底部与大屏右下角。Toast 支持堆叠、展开、滑动退出、键盘焦点和 `success`、`info`、`warning`、`error`、`loading` 类型图标。
## 发送 Toast {#send-toast}
使用模块导出的 manager:
```tsx
import { Button } from "@workspace/ui/components/button"
import { toast } from "@workspace/ui/components/toast"
;<Button
onClick={() =>
toast.add({
title: "已保存",
description: "个人资料已经更新。",
type: "success",
})
}
>
保存
</Button>
```
需要隔离通知区域时,用 `createToastManager()` 创建 manager,并同时传给 `Toaster` 与调用方。关闭按钮的默认可访问名称来自 UI 本地化上下文。
## 消息组合 {#message-composition}
```tsx
import {
Message,
MessageContent,
MessageFooter,
MessageGroup,
} from "@workspace/ui/components/message"
import { Bubble, BubbleContent } from "@workspace/ui/components/bubble"
;<MessageGroup>
<Message align="end">
<MessageContent>
<Bubble align="end" variant="tinted">
<BubbleContent>明天下午三点可以。</BubbleContent>
</Bubble>
<MessageFooter>已读 · 14:32</MessageFooter>
</MessageContent>
</Message>
</MessageGroup>
```
把消息 ID、作者、发送状态与操作保留在业务层。UI primitives 只从 props 和 `data-*` 状态决定布局。
## 附件状态 {#attachments}
`Attachment` 支持 `idle`、`uploading`、`processing`、`error` 和 `done`。`AttachmentMedia`、`AttachmentTitle`、`AttachmentDescription` 与 `AttachmentActions` 会根据状态调整外观;上传进度和重试逻辑仍由应用负责。
横向附件使用默认 `orientation="horizontal"`,缩略图网格可使用 `orientation="vertical"`。多个附件放入 `AttachmentGroup` 后会获得横向滚动和 snap 行为。
## 消息滚动 {#scrolling}
`MessageScrollerProvider`、`MessageScroller`、`MessageScrollerViewport` 与 `MessageScrollerContent` 应保持层级。每条消息放在 `MessageScrollerItem` 中;需要作为自动跟随目标的项传 `scrollAnchor`。`MessageScrollerButton direction="end"` 会根据当前可滚动状态自动显示或隐藏。
业务层追加消息时不要直接设置 `scrollTop`。使用这些 primitives 提供的 follow 和 visibility 状态,避免用户向上阅读历史内容时被强制拉回底部。
+113
View File
@@ -0,0 +1,113 @@
---
title: 浮层与定位
description: 使用 Base UI 的 render 组合 Dialog、Popover 与本地 AnchoredSurface。
order: 12
toc:
- id: dialog
title: Dialog
- id: popover
title: Popover
- id: anchored-surface
title: 连续尾巴
- id: positioning
title: 自定义定位
- id: choosing-an-overlay
title: 选择浮层
---
## Dialog {#dialog}
```tsx
import { Button } from "@workspace/ui/components/button"
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@workspace/ui/components/dialog"
export function ProfileDialog() {
return (
<Dialog>
<DialogTrigger render={<Button variant="outline" />}>
编辑资料
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>编辑资料</DialogTitle>
<DialogDescription>更新公开显示的信息。</DialogDescription>
</DialogHeader>
<div>表单内容</div>
<DialogFooter>
<Button type="submit">保存</Button>
</DialogFooter>
</DialogContent>
</Dialog>
)
}
```
`DialogContent` 已包含 Portal、Overlay 和默认关闭按钮。标题与说明会连接到底层可访问性属性,不要只用无语义的 `div` 替代。
## Popover {#popover}
```tsx
import { Button } from "@workspace/ui/components/button"
import {
Popover,
PopoverContent,
PopoverDescription,
PopoverTitle,
PopoverTrigger,
} from "@workspace/ui/components/popover"
;<Popover>
<PopoverTrigger render={<Button variant="outline" />}>
打开设置
</PopoverTrigger>
<PopoverContent align="end">
<PopoverTitle>显示设置</PopoverTitle>
<PopoverDescription>选择列表密度。</PopoverDescription>
</PopoverContent>
</Popover>
```
本地 API 继续转发 Base UI Popup 与 Positioner 的 props。需要受控状态时给 `Popover` 传入 `open` 和 `onOpenChange`。
## 连续尾巴 {#anchored-surface}
本仓库的 `showArrow` 不是普通三角 SVG。它让 `PopoverContent` 用 `AnchoredSurface` 渲染一个圆角表面与连续曲线尾巴:
```tsx
<PopoverContent
side="bottom"
showArrow={{ cornerRadius: "2xl", tailSize: "sm" }}
className="w-80 p-0"
viewportClassName="max-h-72 p-4"
>
{content}
</PopoverContent>
```
外层表面保持 `overflow: visible`,避免尾巴被裁剪;滚动发生在内部 `data-slot="popover-viewport"`。因此要限制内容高度或设置 padding 时,优先使用 `viewportClassName`。
`AnchoredSurface` 也可以独立使用,但它只负责视觉表面,不负责测量锚点、碰撞检测、焦点或 Portal。
## 自定义定位 {#positioning}
`PopoverContent` 额外暴露 `anchor`、`positionMethod`、`arrowPadding`、`alignOffset` 和 `sideOffset`。虚拟锚点或自定义锚点应通过这些参数传给共享组件,不需要绕过它重新拼装 Base UI primitives。
需要从非 React 代码控制弹层时,可使用 `createPopoverHandle<Payload>()` 创建 handle,再按 Base UI 的 handle 模式传给 Root。
## 选择浮层 {#choosing-an-overlay}
- 必须阻止背景交互:`Dialog` 或 `AlertDialog`。
- 从屏幕边缘进入的任务面板:`Sheet`;强调触摸拖拽的移动面板:`Drawer`。
- 与一个触发点关联的交互内容:`Popover`。
- 只在悬停或聚焦时补充预览:`HoverCard`。
- 简短、非交互说明:`Tooltip`。
基础用法可与 [shadcn Popover 文档](https://ui.shadcn.com/docs/components/base/popover)对照;组合语法必须使用 Base UI 版本的 `render`。
+79 -27
View File
@@ -1,39 +1,91 @@
---
title: 项目配置
description: 导入全局样式,并从显式组件子路径开始使用。
description: 配置 workspace 依赖、全局样式、Tailwind 扫描和 shadcn CLI。
order: 10
toc:
- id: import-styles
title: 导入样式
- id: import-a-component
title: 导入组件
- id: entry-points
title: 包入口
- id: dependency
title: 添加依赖
- id: global-styles
title: 全局样式
- id: imports
title: 显式导入
- id: shadcn-cli
title: shadcn CLI
- id: browser-components
title: 浏览器组件
- id: verify
title: 验证配置
---
## 导入样式 {#import-styles}
## 添加依赖 {#dependency}
在应用入口中导入一次全局样式:
应用通过 workspace 协议引用 UI 包:
```ts
import "@workspace/ui/globals.css"
```
它提供组件所需的共享 Tailwind 层、设计变量、字体配置和工具类。
## 导入组件 {#import-a-component}
```tsx
import { Button } from "@workspace/ui/components/button"
export function SaveButton() {
return <Button>保存</Button>
```json
{
"dependencies": {
"@workspace/ui": "workspace:*"
}
}
```
## 包入口 {#entry-points}
仓库中的应用已经完成此配置。这个 package 当前是私有 workspace package,不需要从 npm 安装。
- `@workspace/ui/components/*` 导出单个组件模块。
- `@workspace/ui/hooks/*` 导出可复用的交互和媒体查询状态。
- `@workspace/ui/lib/utils` 导出共享的类名组合工具。
- `@workspace/ui/locales/*` 导出指定语言的词典。
## 全局样式 {#global-styles}
在应用的全局 CSS 中导入一次:
```css
@import "@workspace/ui/globals.css";
```
该入口会加载 Tailwind CSS、shadcn 的 Tailwind 扩展、动画工具、Inter Variable 字体、语义颜色、圆角变量和基础样式。主题通过根节点上的 `.ui:light` 与 `.ui:dark` class 切换;组件代码不要自行复制这些变量。
`globals.css` 中的 `@source` 已覆盖本仓库的 `apps` 与组件目录。如果将 package 移到另一种目录结构,应同步调整扫描路径,否则生产构建可能缺少组件使用的 utility。
## 显式导入 {#imports}
package 没有聚合式 barrel。始终从具体子路径导入,便于追踪依赖并避免意外扩大客户端 bundle:
```tsx
import { Button } from "@workspace/ui/components/button"
import { SaveIcon } from "@workspace/ui/components/icon"
import { useIsMobile } from "@workspace/ui/hooks/use-breakpoint"
import { cn } from "@workspace/ui/lib/utils"
```
公开入口包括:
- `@workspace/ui/components/*`:组件与局部类型。
- `@workspace/ui/hooks/*`:断点和波纹交互。
- `@workspace/ui/locales`:支持的 locale 与消息 ID 类型。
- `@workspace/ui/locales/en-US`、`@workspace/ui/locales/zh-Hans`:消息与日历 locale。
- `@workspace/ui/globals.css`:全局样式。
## shadcn CLI {#shadcn-cli}
仓库根目录运行:
```sh
bunx shadcn@latest add button -c apps/web
```
应用和 UI package 的 `components.json` 使用同一套 `base-vega`、neutral、CSS variables、Lucide 词汇和 RTL 配置;别名会把生成文件放到 `packages/ui/src/components`。这与 shadcn 官方的 [monorepo 工作流](https://ui.shadcn.com/docs/monorepo)一致。
CLI 生成完成后必须检查 diff。生成器不知道本仓库的 Icones、国际化、波纹和 Popover 扩展,更新已有组件时可能覆盖本地适配。`components.json` 各字段的含义可参考[官方说明](https://ui.shadcn.com/docs/components-json)。
## 浏览器组件 {#browser-components}
多数交互组件依赖事件、Portal 或浏览器 API。使用 React Server Components 的消费端应从自身的客户端边界导入它们。本仓库配置为 `rsc: false`,组件文件不会统一插入框架专用指令;少数组件因自身需要显式包含 `"use client"`。
## 验证配置 {#verify}
修改组件或配置后运行:
```sh
bun run --cwd packages/ui typecheck
bun run lint
bun run format:check
```
如果改动涉及消费方式,再运行应用或文档的构建,确认 Tailwind 能扫描到新 class,并检查浅色、深色与 RTL。
+39 -10
View File
@@ -1,24 +1,53 @@
---
title: 概览
description: 了解 UI 包的定位,以及显式入口如何支持组件组合。
description: 了解 @workspace/ui 的职责、上游基线和本仓库的扩展。
order: 1
toc:
- id: package-role
title: 包的职责
- id: tutorial-map
title: 教程路线
- id: ui-or-blocks
title: UI 还是 Blocks
- id: source-model
title: 源码模型
- id: local-adaptations
title: 本地适配
- id: learning-path
title: 学习路线
- id: package-boundaries
title: 包边界
---
## 包的职责 {#package-role}
`@workspace/ui` 包含可复用的 React 基础组件、复合控件、Hooks、图标、样式和本地化词典。它是应用与上层功能包共享的视觉基础。
`@workspace/ui` 是整个工作区的视觉基础。它提供可编辑的 React 组件源码、Tailwind CSS 主题变量、响应式与交互 Hooks、统一图标入口,以及组件自身使用的本地化消息。
## 教程路线 {#tutorial-map}
这个包只负责通用界面能力。路由、数据请求、持久化和业务流程仍由应用或领域 package 管理。
本教程会安装样式、介绍组件子路径、通过组合构建一个小型表单、添加响应式行为,并接入本地化词典。
## 源码模型 {#source-model}
## UI 还是 Blocks {#ui-or-blocks}
组件以 [shadcn/ui Base UI 版本](https://ui.shadcn.com/docs/components/base)和其 [GitHub registry 源码](https://github.com/shadcn-ui/ui/tree/main/apps/v4/registry/bases/base/ui)为基线。与 shadcn/ui 一样,它们不是不可修改的第三方黑盒:源码位于 `packages/ui/src`,可以直接审查、组合和演进。
当应用自己拥有业务流程时使用 UI 组件。导航和通知等完整应用外壳功能使用 `@workspace/blocks`,媒体与搜索等领域工作流使用 `@workspace/media` 或 `@workspace/search` 等专用包。
本仓库使用 `base-vega` 风格、CSS 变量、Tailwind CSS v4、Base UI primitives 和 RTL 输出。`components.json` 只控制生成方式;运行时 API 以本仓库源码为准。
## 本地适配 {#local-adaptations}
本仓库在上游基础上做了这些有意保留的调整:
- 使用 `@icones/react` 封装统一的 `components/icon` 入口,不直接依赖 `lucide-react`。
- `Button` 默认接入指针波纹反馈。
- 增加 `I18nProvider`,让 Breadcrumb、Calendar、Carousel、Pagination、Sidebar、Toast 等组件使用应用的翻译器。
- 只发布 `en-US` 和 `zh-Hans` 两套 UI 词典与日历 locale。
- 增加 mobile、tablet、desktop 三段式断点 Hooks 和根节点 class 同步。
- 扩展 `PopoverContent` 的自定义锚点、定位方式、连续尾巴和独立滚动视口。
- 增加 `AnchoredSurface`,并保留消息、气泡、附件和消息滚动器等组合组件。
升级 shadcn/ui 组件时不要盲目覆盖这些差异。完整清单见“本仓库适配”。
## 学习路线 {#learning-path}
先完成“项目配置”,再通过“组件目录”确定入口;浮层和反馈类组件有独立指南。需要 SSR、响应式行为或多语言时,再阅读进阶章节。示例章节展示可直接组合进应用的完整结构。
## 包边界 {#package-boundaries}
- 使用 `@workspace/ui` 构建按钮、表单、菜单、弹层、列表和反馈等基础界面。
- 使用 `@workspace/blocks` 组合应用外壳、导航、聊天和通知等完整区域。
- 使用 `@workspace/media`、`@workspace/search`、`@workspace/lexical` 等 package 承载领域交互。
- 使用 `@workspace/appearance` 与 `@workspace/preferences` 聚合主题偏好和持久化。