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,66 @@
---
title: Anchored account menu
description: Compose an account menu with Popover's continuous tail, scroll viewport, and Base UI render prop.
order: 31
toc:
- id: example
title: Example
- id: why-these-props
title: Why these props
- id: accessibility
title: Accessibility
---
## Example {#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="Account menu" />
}
>
<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 />
Appearance
</Button>
</PopoverContent>
</Popover>
)
}
```
## Why these props {#why-these-props}
`align="end"` aligns the logical end of the panel and trigger, adapting automatically in RTL. `showArrow` enables the local continuous tail. The outer `className` styles the whole surface while `viewportClassName` styles its scrolling region, allowing a long menu to scroll without clipping the tail.
## Accessibility {#accessibility}
Provide an `aria-label` when the trigger button contains only an icon or avatar. Use `PopoverTitle` and `PopoverDescription` in the panel, and let the Base UI Root own dismissal and focus return. If the content is semantically a set of choices or commands, use DropdownMenu or Command rather than assigning a menu role manually.
+81 -20
View File
@@ -1,37 +1,98 @@
---
title: Compose a form
description: Combine field, input, and button primitives while keeping domain state in the application.
title: Compose a profile form
description: Combine Field, Input, Select, and Button into an accessible form while keeping state in the application.
order: 30
toc:
- id: create-the-form
title: Create the form
- id: complete-example
title: Complete example
- id: validation
title: Validation
title: Validation state
- id: controlled-select
title: Controlled Select
- id: styling
title: Styling
title: Style overrides
---
## Create the form {#create-the-form}
## Complete example {#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">Display name</FieldLabel>
<Input id="display-name" name="displayName" />
<FieldError>{errors.displayName}</FieldError>
</Field>
<Button type="submit">Save profile</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") ? "" : "Enter a display name")
}
return (
<form onSubmit={submit} className="max-w-md space-y-6">
<FieldGroup>
<Field data-invalid={Boolean(error)}>
<FieldLabel htmlFor="display-name">Display name</FieldLabel>
<Input
id="display-name"
name="displayName"
aria-invalid={Boolean(error)}
aria-describedby="display-name-error"
/>
<FieldDescription>
This name is visible to other members.
</FieldDescription>
<FieldError id="display-name-error">{error}</FieldError>
</Field>
<Field>
<FieldLabel htmlFor="role">Role</FieldLabel>
<Select
value={role}
onValueChange={(value) => setRole(value ?? "editor")}
>
<SelectTrigger id="role">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="viewer">Viewer</SelectItem>
<SelectItem value="editor">Editor</SelectItem>
<SelectItem value="admin">Administrator</SelectItem>
</SelectContent>
</Select>
</Field>
</FieldGroup>
<Button type="submit">Save profile</Button>
</form>
)
}
```
## Validation {#validation}
## Validation state {#validation}
The UI package renders validation state but does not select a form library or schema. Connect native form data, React state, or a form framework at the application boundary.
The UI package does not select a validation library. The application produces errors and connects the same state to `Field data-invalid`, the control's `aria-invalid`, and `FieldError`. Multiple schema errors can be passed through `FieldError errors={errors}`; the component deduplicates them before rendering a list.
## Styling {#styling}
## Controlled Select {#controlled-select}
Use component variants for supported semantic changes and `className` for local layout. Use `cn` from `@workspace/ui/lib/utils` when conditional classes or consumer overrides must be merged.
The local Select is built on Base UI and uses `value` with `onValueChange`. `SelectTrigger` renders its own arrow, so it only needs `SelectValue`; do not add another chevron. The popup aligns to the trigger and adopts its width by default. Adjust `side`, `align`, `sideOffset`, and `alignItemWithTrigger` on `SelectContent` when required.
## Style overrides {#styling}
Use the existing `variant`, `size`, and structural parts first, then apply page layout through `className`. Components merge classes with `cn`, allowing consumer Tailwind classes to override default utilities. Promote overrides repeated across pages into a new variant or composite component instead of copying long class strings.
@@ -0,0 +1,58 @@
---
title: Message thread
description: Compose a scroll container, message layout, and bubbles while preserving the reader's position in history.
order: 32
toc:
- id: structure
title: Component structure
- id: messages
title: Render messages
- id: ownership
title: State ownership
---
## Component structure {#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>
)
}
```
## Render messages {#messages}
Inside `ChatMessageRow`, use `Message`, `MessageAvatar`, `MessageContent`, `MessageHeader`, and `MessageFooter` for participant structure, then compose one or more `Bubble` elements inside the content. Sent messages can use `align="end"`; do not swap message ownership based on the language direction.
Place attachments in the same `MessageContent`. For content without a bubble background, use `Bubble variant="ghost"`; the header and footer automatically remove the horizontal padding associated with a bubble.
## State ownership {#ownership}
`MessageScroller` owns bottom-follow behavior, visibility, and the jump button. The application owns the message array, pagination, loading, read state, and retries. Keep stable IDs when prepending older messages and use the scroller API to preserve the visual anchor instead of copying another set of scroll listeners into the feature.