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: Add responsive behavior
description: Use shared breakpoint and interaction hooks when CSS alone cannot express the behavior.
title: Responsive and interaction hooks
description: Synchronize breakpoints when behavior must follow the viewport and add ripple feedback to custom controls.
order: 20
toc:
- id: breakpoints
title: Breakpoints
- id: breakpoint-contract
title: Breakpoint contract
- id: reading-breakpoints
title: Read breakpoints
- id: root-classes
title: Root classes
- id: server-rendering
title: Server rendering
- id: ripple
title: Ripple interaction
---
## Breakpoints {#breakpoints}
## Breakpoint contract {#breakpoint-contract}
The shared breakpoint splits the viewport into three ranges:
| Value | Range |
| --------- | -------------------- |
| `mobile` | Below 768px |
| `tablet` | 768px through 1023px |
| `desktop` | 1024px and above |
Prefer CSS for visual changes. Read a breakpoint during rendering only when mounted content, interaction behavior, or data volume must change.
## Read breakpoints {#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()
```
Prefer CSS responsive variants for visual changes. Use these hooks only when component behavior or mounted content must change.
Every hook subscribes to the same pair of `matchMedia` queries through `useSyncExternalStore`. `useOnBreakpoint(listener)` runs only after the breakpoint changes; it does not report the initial value as a change.
## Root classes {#root-classes}
After mounting `BreakpointReactor`, the document root has exactly one of `.ui:mobile`, `.ui:tablet`, or `.ui:desktop`:
```tsx
import { BreakpointReactor } from "@workspace/ui/hooks/use-breakpoint"
;<>
<BreakpointReactor />
<App />
</>
```
`globals.css` registers these as the custom `mobile:`, `tablet:`, and `desktop:` variants, allowing CSS and behavioral code to share the same three-range definition.
## Server rendering {#server-rendering}
The breakpoint store uses a deterministic desktop server snapshot. `getBreakpointInitializationScript` can assign the matching root class before hydration when the application needs CSS and behavioral breakpoints to agree immediately.
The server snapshot is fixed to `desktop` so SSR remains deterministic. To write the real class before hydration, place the result of `getBreakpointInitializationScript()` in an inline script in the document head:
```tsx
import { getBreakpointInitializationScript } from "@workspace/ui/hooks/use-breakpoint"
;<script
dangerouslySetInnerHTML={{ __html: getBreakpointInitializationScript() }}
/>
```
Pass the application's nonce under a strict CSP. The script only synchronizes the initial class; it does not replace `BreakpointReactor` for later viewport changes.
## Ripple interaction {#ripple}
`useRipple` returns a ref callback that adds pointer-driven feedback to an interactive element. The element should establish a positioned containing block so the generated overlay uses the correct bounds.
`Button` already uses `useRippleRef`, so ordinary buttons need no extra setup. Use the hook directly for a custom interactive surface:
```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">
Open project
</button>
)
}
```
The element must establish a positioned containing block, normally with `relative`. The hook ignores disabled controls, `aria-disabled="true"`, and non-primary mouse buttons, and removes listeners and animation nodes when the ref changes or unmounts. Use `useRippleRef(ref, options)` when merging an external ref.
@@ -1,19 +1,29 @@
---
title: Localize UI
description: Load only the UI catalog and calendar locale required by the active language.
description: Bridge the application translator into UI components and load the active calendar locale.
order: 21
toc:
- id: catalog-source
title: Catalog source
- id: direct-imports
title: Direct imports
- id: supported-locales
title: Supported locales
- id: two-layers
title: Two responsibilities
- id: catalogs
title: Merge catalogs
- id: bridge-provider
title: Bridge the provider
- id: fallback
title: Fallback behavior
- id: direction
title: Text direction
---
## Catalog source {#catalog-source}
## Two responsibilities {#two-layers}
Add the UI locale source to the application's internationalization configuration:
`@workspace/i18n` manages the application locale, catalog loading, formatting, and switching. `@workspace/ui/components/i18n` is a small adapter that keeps UI primitives independent of an internationalization framework. It accepts a compatible translator, a language tag, and a `react-day-picker` locale.
This separation keeps the UI package reusable while allowing applications to use the full workspace internationalization runtime.
## Merge catalogs {#catalogs}
Add the UI catalog source to the application's `i18n.config.json`:
```json
{
@@ -21,16 +31,50 @@ Add the UI locale source to the application's internationalization configuration
}
```
The locales root contains metadata and types. It does not import every language catalog.
The `@workspace/ui/locales` root exports only `uiCatalogLocales`, `uiMessageIds`, and types; it does not statically import every language. The current supported set is `en-US` and `zh-Hans`.
## Direct imports {#direct-imports}
## Bridge the provider {#bridge-provider}
```ts
import { calendarLocale, messages } from "@workspace/ui/locales/en-US"
Read the translator from the current application provider and pass it to the 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>
)
}
```
Each language entry also exports its matching `react-day-picker` locale.
`Calendar` prefers its own `locale` prop, then the provider's `calendarLocale`. Month formatting and the default `lang` use the provider language tag.
## Supported locales {#supported-locales}
## Fallback behavior {#fallback}
The UI catalogs include American English (`en-US`) and Simplified Chinese (`zh-Hans`).
Without a provider, the UI layer uses `en-US` and the English source text from each descriptor. Isolated tests and stories therefore remain usable, but production applications should bridge the provider so accessible names and visible text use the same locale.
Custom components may use `uiMessages`, `useMessage`, `useTranslate`, `LocalizedText`, `useLocale`, and `useCalendarLocale`. Product copy belongs in the application or domain package, not in the UI catalog.
## Text direction {#direction}
Translation locale and layout direction are separate concerns. When switching locales, the application should synchronize `document.documentElement.lang` and `dir`, and use `DirectionProvider` when needed. Local components use logical utilities such as `start`, `end`, `ps`, and `pe`; `components.json` also enables `rtl` generation.
@@ -0,0 +1,67 @@
---
title: Workspace adaptations
description: Record the implementation differences that must survive updates from the shadcn/ui Base UI source.
order: 22
toc:
- id: baseline
title: Upstream baseline
- id: icons-and-ripple
title: Icons and ripple
- id: localization
title: Localization
- id: popover
title: Popover extensions
- id: select
title: Select correction
- id: upgrade-checklist
title: Upgrade checklist
---
## Upstream baseline {#baseline}
The local components began from shadcn/ui's Base UI registry with the `base-vega` style. Use the upstream [component documentation](https://ui.shadcn.com/docs/components/base) and [GitHub source directory](https://github.com/shadcn-ui/ui/tree/main/apps/v4/registry/bases/base/ui) as references, but do not assume the latest upstream version is identical to the workspace snapshot.
When determining behavior, prefer the local source, then this documentation, then local `components.json`, and only then the latest upstream example.
## Icons and ripple {#icons-and-ripple}
`components/icon.tsx` maps Lucide icon names to `@icones/react` components and preserves a set of upstream-compatible export names. Existing components import through that entry point. Convert any `lucide-react` import introduced by the CLI back to the local module.
`Button` merges its incoming ref with `useRippleRef`. Preserve relative positioning, ref merging, and disabled behavior when updating Button.
## Localization {#localization}
Breadcrumb, Calendar, Carousel, Dialog, Pagination, Sheet, Sidebar, Spinner, Toast, and related components replace some hard-coded English with descriptors from `components/i18n.tsx`. When updating these files, inspect accessible labels, screen-reader-only text, and default descriptions in addition to visible UI.
Only `en-US` and `zh-Hans` catalogs are published. Adding a message requires synchronized changes to:
1. The `uiMessages` descriptor.
2. The `uiMessageIds` type list.
3. `messages` in both locale modules.
4. The component that consumes the message.
## Popover extensions {#popover}
The local `PopoverContent` adds these capabilities beyond the upstream wrapper:
- Forwarded `anchor` and `positionMethod` options.
- `showArrow`, accepting an `AnchoredSurface` configuration object.
- `viewportClassName`, separating scrollable content from the overflow-visible outer surface.
- An anchor-sized default side offset and continuous curved tail.
- `createPopoverHandle` and `PopoverHandle` exports.
Do not replace the file wholesale with upstream source. In particular, test long-content scrolling, every side, collision flips, narrow triggers, and RTL.
## Select correction {#select}
`SelectTrigger` renders `ChevronDownIcon` as a child of `SelectPrimitive.Icon` instead of passing the icon component through `render`. This correction is intentional for the current Base UI and Icones combination. After a Select update, verify that the trigger shows exactly one icon and test SSR and hydration.
## Upgrade checklist {#upgrade-checklist}
1. Review the shadcn docs and matching registry source diff before replacing anything.
2. Generate through the CLI on a temporary branch or stage files individually, preserving local public exports.
3. Convert direct icon dependencies back to `components/icon`.
4. Restore translated messages, logical-direction utilities, and local theme classes.
5. Give Button, Popover, Select, Calendar, Toast, and Sidebar focused regression coverage.
6. Run the UI typecheck, repository lint and format checks, then build a real consuming application.
7. Manually inspect light, dark, LTR, RTL, mobile, tablet, and desktop behavior.
@@ -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.
+87 -14
View File
@@ -1,30 +1,103 @@
---
title: Choose components
description: Select primitives by responsibility instead of importing a single monolithic component index.
title: Component catalog
description: Find component entry points by responsibility and understand the local Base UI composition model.
order: 11
toc:
- id: forms
title: Forms and input
- id: overlays
title: Overlays and menus
- id: content
- id: composition-model
title: Composition model
- id: actions-and-forms
title: Actions and forms
- id: overlays-and-navigation
title: Overlays and navigation
- id: content-and-feedback
title: Content and feedback
- id: layout-and-data
title: Layout and data
- id: icons
title: Icons
---
## Forms and input {#forms}
## Composition model {#composition-model}
Use `Button`, `Input`, `Textarea`, `Field`, and `Label` for ordinary forms. Add `Checkbox`, `RadioGroup`, `Switch`, or `Slider` for choices, and `Select`, `Combobox`, `Command`, or `Calendar` for richer selection.
The components follow shadcn/ui's model of composing primitives from editable source, with the Base UI implementation selected in this workspace. A component is usually split into Root, Trigger, Content, Item, and related parts. The underlying primitive owns state and accessibility, while Tailwind classes and `data-slot` expose presentation.
## Overlays and menus {#overlays}
Base UI uses `render` to compose a custom element, rather than the `asChild` prop commonly seen in Radix examples:
`Dialog`, `AlertDialog`, `Sheet`, and `Drawer` cover modal surfaces. `Popover`, `HoverCard`, and `Tooltip` provide anchored information, while the menu modules cover context, dropdown, menubar, and navigation patterns.
```tsx
import { Button } from "@workspace/ui/components/button"
import { Dialog, DialogTrigger } from "@workspace/ui/components/dialog"
## Content and feedback {#content}
;<Dialog>
<DialogTrigger render={<Button variant="outline" />}>
Edit profile
</DialogTrigger>
</Dialog>
```
Cards, items, tables, charts, progress, skeletons, spinners, empty states, messages, and toasts provide the common display vocabulary. Accordion, Collapsible, Tabs, Carousel, and Pagination organize larger content sets.
Use `render` only when replacing the underlying element. Pass ordinary content as children.
## Actions and forms {#actions-and-forms}
| Goal | Component entry points |
| --------------------------- | -------------------------------------------------- |
| Buttons and grouped actions | `button`, `button-group`, `toggle`, `toggle-group` |
| Text input | `input`, `textarea`, `input-group`, `input-otp` |
| Field structure | `field`, `label` |
| Boolean and single choice | `checkbox`, `switch`, `radio-group` |
| Range and progress | `slider`, `progress` |
| Options and commands | `native-select`, `select`, `combobox`, `command` |
| Dates | `calendar` |
Prefer `NativeSelect` for simple, mobile-friendly choices that do not need a custom popup. Choose `Combobox`, `Select`, or `Command` when the interaction needs search, custom items, or command behavior.
## Overlays and navigation {#overlays-and-navigation}
| Goal | Component entry points |
| ------------------------------ | -------------------------------------------------------- |
| Modal confirmation and editing | `alert-dialog`, `dialog`, `sheet`, `drawer` |
| Anchored content | `popover`, `hover-card`, `tooltip`, `anchored-surface` |
| Menus | `dropdown-menu`, `context-menu`, `menubar` |
| Page navigation | `breadcrumb`, `navigation-menu`, `pagination`, `sidebar` |
Dialog, Sheet, and Drawer expose controlled state and composable parts. Use `AlertDialog` for consequential destructive actions, `Tooltip` for brief descriptions, and `Popover` for an anchored panel containing interactive controls.
## Content and feedback {#content-and-feedback}
| Goal | Component entry points |
| ------------------------- | ----------------------------------------------------- |
| Containers and states | `card`, `item`, `empty`, `alert`, `badge`, `marker` |
| Loading and notifications | `skeleton`, `spinner`, `toast` |
| Messaging UI | `message`, `bubble`, `attachment`, `message-scroller` |
| Sectioned content | `accordion`, `collapsible`, `tabs`, `carousel` |
`Message` owns participant layout, alignment, and metadata; `Bubble` owns the message surface; `Attachment` expresses an upload lifecycle; and `MessageScroller` handles follow mode and jump controls. They are presentation primitives and do not own conversation data.
## Layout and data {#layout-and-data}
| Goal | Component entry points |
| -------------- | ------------------------------------------------------- |
| Data display | `table`, `chart` |
| Layout | `separator`, `aspect-ratio`, `resizable`, `scroll-area` |
| Direction | `direction` |
| Keyboard hints | `kbd` |
| Avatars | `avatar` |
The chart module wraps Recharts containers, tooltips, legends, and style mappings without choosing an application data model. `DirectionProvider` re-exports Base UI's direction context; the page should still synchronize the HTML `dir` attribute.
## Icons {#icons}
Import named icons from `@workspace/ui/components/icon`. The module centralizes the `@icones/react` vocabulary so applications and packages use the same icon source.
Import named icons from the local Icones adapter:
```tsx
import { SearchIcon, SettingsIcon } from "@workspace/ui/components/icon"
```
Components provide a default size to SVGs that do not have an explicit size class. Icon buttons still require an accessible name:
```tsx
<Button size="icon" aria-label="Search">
<SearchIcon aria-hidden="true" />
</Button>
```
Do not reintroduce `lucide-react` in new components. Register additional icons through `createIcon` in `components/icon.tsx` so the workspace keeps one icon path.
+95
View File
@@ -0,0 +1,95 @@
---
title: Feedback and messaging
description: Configure Toast and compose messaging interfaces from Message, Bubble, Attachment, and MessageScroller.
order: 13
toc:
- id: toast-provider
title: Toast Provider
- id: send-toast
title: Send a toast
- id: message-composition
title: Message composition
- id: attachments
title: Attachment states
- id: scrolling
title: Message scrolling
---
## Toast Provider {#toast-provider}
Mount one `Toaster` near the application root:
```tsx
import { Toaster } from "@workspace/ui/components/toast"
export function App() {
return (
<>
<Routes />
<Toaster />
</>
)
}
```
The default viewport is fixed to the bottom on small screens and the bottom-right on larger screens. Toasts support stacking, expansion, swipe dismissal, keyboard focus, and icons for `success`, `info`, `warning`, `error`, and `loading` types.
## Send a toast {#send-toast}
Use the manager exported by the module:
```tsx
import { Button } from "@workspace/ui/components/button"
import { toast } from "@workspace/ui/components/toast"
;<Button
onClick={() =>
toast.add({
title: "Saved",
description: "Your profile has been updated.",
type: "success",
})
}
>
Save
</Button>
```
When notifications need an isolated scope, create a manager with `createToastManager()` and pass it to both `Toaster` and the caller. The default accessible name of the close button comes from the UI localization context.
## Message composition {#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>Tomorrow at three works for me.</BubbleContent>
</Bubble>
<MessageFooter>Read · 14:32</MessageFooter>
</MessageContent>
</Message>
</MessageGroup>
```
Keep message IDs, authors, delivery state, and actions in the product layer. The UI primitives derive layout only from props and `data-*` state.
## Attachment states {#attachments}
`Attachment` supports `idle`, `uploading`, `processing`, `error`, and `done`. `AttachmentMedia`, `AttachmentTitle`, `AttachmentDescription`, and `AttachmentActions` adapt their presentation to the state; the application still owns progress and retry behavior.
Use the default `orientation="horizontal"` for rows and `orientation="vertical"` for thumbnail grids. `AttachmentGroup` adds horizontal scrolling and snap behavior for a collection.
## Message scrolling {#scrolling}
Keep `MessageScrollerProvider`, `MessageScroller`, `MessageScrollerViewport`, and `MessageScrollerContent` in that hierarchy. Wrap each message in `MessageScrollerItem`, adding `scrollAnchor` to the item that should anchor follow mode. `MessageScrollerButton direction="end"` reveals and hides itself from the current scroll state.
Do not assign `scrollTop` directly when appending messages. Use the follow and visibility state exposed by these primitives so a user reading older messages is not forced back to the bottom.
+113
View File
@@ -0,0 +1,113 @@
---
title: Overlays and positioning
description: Compose Dialog and Popover with Base UI render semantics and the local AnchoredSurface.
order: 12
toc:
- id: dialog
title: Dialog
- id: popover
title: Popover
- id: anchored-surface
title: Continuous tail
- id: positioning
title: Custom positioning
- id: choosing-an-overlay
title: Choose an overlay
---
## 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" />}>
Edit profile
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Edit profile</DialogTitle>
<DialogDescription>Update your public information.</DialogDescription>
</DialogHeader>
<div>Form content</div>
<DialogFooter>
<Button type="submit">Save</Button>
</DialogFooter>
</DialogContent>
</Dialog>
)
}
```
`DialogContent` already includes the portal, overlay, and default close button. The title and description connect to the underlying accessibility attributes, so do not replace them with unstructured `div` elements.
## 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" />}>
Open settings
</PopoverTrigger>
<PopoverContent align="end">
<PopoverTitle>Display settings</PopoverTitle>
<PopoverDescription>Choose the list density.</PopoverDescription>
</PopoverContent>
</Popover>
```
The local API continues to forward Base UI Popup and Positioner props. Pass `open` and `onOpenChange` to `Popover` when controlled state is required.
## Continuous tail {#anchored-surface}
The local `showArrow` is more than a triangular SVG. It renders `PopoverContent` through `AnchoredSurface`, producing one rounded surface with a continuous curved tail:
```tsx
<PopoverContent
side="bottom"
showArrow={{ cornerRadius: "2xl", tailSize: "sm" }}
className="w-80 p-0"
viewportClassName="max-h-72 p-4"
>
{content}
</PopoverContent>
```
The outer surface remains `overflow: visible` so the tail is not clipped. Scrolling happens inside `data-slot="popover-viewport"`. Use `viewportClassName` when constraining content height or applying content padding.
`AnchoredSurface` can also render on its own, but it is visual only: it does not own anchor measurement, collision detection, focus, or a portal.
## Custom positioning {#positioning}
`PopoverContent` additionally exposes `anchor`, `positionMethod`, `arrowPadding`, `alignOffset`, and `sideOffset`. Pass virtual or custom anchors through the shared component instead of rebuilding the Base UI primitives.
For control outside React, create a handle with `createPopoverHandle<Payload>()` and connect it to the Root using Base UI's handle pattern.
## Choose an overlay {#choosing-an-overlay}
- Block background interaction with `Dialog` or `AlertDialog`.
- Use `Sheet` for a task surface entering from a screen edge and `Drawer` for a touch-oriented mobile panel.
- Use `Popover` for interactive content attached to a trigger.
- Use `HoverCard` for a preview revealed by hover or focus.
- Use `Tooltip` for short, non-interactive descriptions.
Compare basic usage with the [shadcn Popover documentation](https://ui.shadcn.com/docs/components/base/popover), but keep the Base UI `render` composition syntax.
+79 -27
View File
@@ -1,39 +1,91 @@
---
title: Project setup
description: Import the global stylesheet and begin with explicit component subpaths.
description: Configure the workspace dependency, global styles, Tailwind scanning, and the shadcn CLI.
order: 10
toc:
- id: import-styles
title: Import styles
- id: import-a-component
title: Import a component
- id: entry-points
title: Entry points
- id: dependency
title: Add the dependency
- id: global-styles
title: Global styles
- id: imports
title: Explicit imports
- id: shadcn-cli
title: shadcn CLI
- id: browser-components
title: Browser components
- id: verify
title: Verify the setup
---
## Import styles {#import-styles}
## Add the dependency {#dependency}
Import the global stylesheet once at the application entry:
Applications reference the UI package through the workspace protocol:
```ts
import "@workspace/ui/globals.css"
```
It provides the shared Tailwind layers, design variables, font setup, and utilities expected by the components.
## Import a component {#import-a-component}
```tsx
import { Button } from "@workspace/ui/components/button"
export function SaveButton() {
return <Button>Save</Button>
```json
{
"dependencies": {
"@workspace/ui": "workspace:*"
}
}
```
## Entry points {#entry-points}
The applications in this repository are already configured this way. The package is currently private to the workspace and does not need to be installed from npm.
- `@workspace/ui/components/*` exposes one component module.
- `@workspace/ui/hooks/*` exposes reusable interactions and media-query state.
- `@workspace/ui/lib/utils` exposes shared class-name composition.
- `@workspace/ui/locales/*` exposes language-specific catalogs.
## Global styles {#global-styles}
Import the stylesheet once from the application's global CSS:
```css
@import "@workspace/ui/globals.css";
```
This entry loads Tailwind CSS, shadcn's Tailwind extension, animation utilities, the Inter Variable font, semantic colors, radius variables, and base styles. Themes switch through `.ui:light` and `.ui:dark` on the root element; component code should not copy these variables.
The `@source` directives in `globals.css` cover this repository's `apps` and component directories. Update those paths if the package moves into another directory layout, or production builds may omit utilities used by components.
## Explicit imports {#imports}
There is no aggregated barrel. Import concrete subpaths so dependencies remain visible and client bundles do not grow accidentally:
```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"
```
Public entry points include:
- `@workspace/ui/components/*` for components and their local types.
- `@workspace/ui/hooks/*` for breakpoint and ripple interactions.
- `@workspace/ui/locales` for supported locales and message ID types.
- `@workspace/ui/locales/en-US` and `@workspace/ui/locales/zh-Hans` for messages and calendar locales.
- `@workspace/ui/globals.css` for global styles.
## shadcn CLI {#shadcn-cli}
Run the command from the repository root:
```sh
bunx shadcn@latest add button -c apps/web
```
The application and UI package `components.json` files agree on `base-vega`, neutral, CSS variables, the Lucide vocabulary, and RTL output. Their aliases place generated files under `packages/ui/src/components`, following shadcn's official [monorepo workflow](https://ui.shadcn.com/docs/monorepo).
Always inspect the diff after generation. The generator does not know about this workspace's Icones layer, localization, ripple behavior, or Popover extensions and can overwrite them when updating an existing component. See the official [`components.json` reference](https://ui.shadcn.com/docs/components-json) for the meaning of each setting.
## Browser components {#browser-components}
Most interactive components depend on events, portals, or browser APIs. Consumers using React Server Components should import them from their own client boundary. This workspace has `rsc: false`; component files do not receive a framework-specific directive globally, although a few components include `"use client"` when they require it.
## Verify the setup {#verify}
After changing a component or configuration, run:
```sh
bun run --cwd packages/ui typecheck
bun run lint
bun run format:check
```
When a change affects consumption, also build the application or docs so Tailwind scanning is exercised, then verify light, dark, and RTL modes.
+39 -10
View File
@@ -1,24 +1,53 @@
---
title: Overview
description: Learn where the UI package fits and how its explicit entry points support composition.
description: Understand the role, upstream baseline, and workspace-specific extensions of @workspace/ui.
order: 1
toc:
- id: package-role
title: Package role
- id: tutorial-map
title: Tutorial map
- id: ui-or-blocks
title: UI or Blocks
- id: source-model
title: Source model
- id: local-adaptations
title: Local adaptations
- id: learning-path
title: Learning path
- id: package-boundaries
title: Package boundaries
---
## Package role {#package-role}
`@workspace/ui` contains reusable React primitives, composite controls, hooks, icons, styles, and locale catalogs. It is the visual foundation shared by applications and higher-level feature packages.
`@workspace/ui` is the visual foundation of the workspace. It provides editable React component source, Tailwind CSS theme variables, responsive and interaction hooks, a shared icon entry point, and the localized messages used by the components themselves.
## Tutorial map {#tutorial-map}
The package owns generic interface behavior only. Routing, data access, persistence, and product workflows remain in the application or a domain package.
This tutorial installs the styles, introduces component subpaths, builds a small form through composition, adds responsive behavior, and connects locale catalogs.
## Source model {#source-model}
## UI or Blocks {#ui-or-blocks}
The components use the [shadcn/ui Base UI implementation](https://ui.shadcn.com/docs/components/base) and its [GitHub registry source](https://github.com/shadcn-ui/ui/tree/main/apps/v4/registry/bases/base/ui) as their baseline. Like shadcn/ui, they are editable source rather than an opaque component dependency: the implementation lives in `packages/ui/src` and can be reviewed, composed, and evolved locally.
Use UI components when the application owns the workflow. Use `@workspace/blocks` for complete application-shell features such as navigation and notifications, and focused packages such as `@workspace/media` or `@workspace/search` for domain workflows.
This workspace selects the `base-vega` style, CSS variables, Tailwind CSS v4, Base UI primitives, and RTL output. `components.json` controls generation; the checked-in workspace source is the runtime authority.
## Local adaptations {#local-adaptations}
The workspace intentionally preserves these changes on top of the upstream baseline:
- A shared `components/icon` entry point backed by `@icones/react`, instead of direct `lucide-react` imports.
- Pointer ripple feedback built into `Button`.
- An `I18nProvider` that localizes Breadcrumb, Calendar, Carousel, Pagination, Sidebar, Toast, and related controls through the application's translator.
- UI catalogs and calendar locales for `en-US` and `zh-Hans` only.
- Mobile, tablet, and desktop breakpoint hooks with root-class synchronization.
- Custom anchors, positioning methods, a continuous tail, and an independent scroll viewport on `PopoverContent`.
- `AnchoredSurface` plus message, bubble, attachment, and message-scroller composition primitives.
Do not overwrite these differences blindly when updating from shadcn/ui. The “Workspace adaptations” chapter contains the full checklist.
## Learning path {#learning-path}
Start with “Project setup”, then use the “Component catalog” to find the right entry point. Overlays and feedback have focused guides. Continue to the advanced chapters when the application needs SSR-safe responsive behavior or localization. The examples assemble the primitives into application-ready structures.
## Package boundaries {#package-boundaries}
- Use `@workspace/ui` for buttons, forms, menus, overlays, lists, and generic feedback.
- Use `@workspace/blocks` for complete application areas such as shells, navigation, chat, and notifications.
- Use domain packages such as `@workspace/media`, `@workspace/search`, and `@workspace/lexical` for domain interaction.
- Use `@workspace/appearance` with `@workspace/preferences` to compose theme preferences and persistence.
+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` 聚合主题偏好和持久化。