feat(overlays): add managed overlay package
This commit is contained in:
@@ -0,0 +1,64 @@
|
||||
---
|
||||
title: Lifecycle and route interception
|
||||
description: Understand results, queues, Host lifetime, and router adapters.
|
||||
order: 20
|
||||
toc:
|
||||
- id: result-contracts
|
||||
title: Result contracts
|
||||
- id: queue
|
||||
title: Queues and Hosts
|
||||
- id: dismissal
|
||||
title: Dismissal
|
||||
- id: navigation
|
||||
title: Route interception
|
||||
---
|
||||
|
||||
## Result contracts {#result-contracts}
|
||||
|
||||
`openConfirm` uses `false` for cancellation, `openPrompt` uses `null`, and custom Modals, Sheets, and Drawers use `undefined` when no result is supplied. `close(value)` is checked against the generic passed to `openModal<Result>()`.
|
||||
|
||||
```tsx
|
||||
import { openModal } from "@workspace/overlays/modal"
|
||||
|
||||
const productId = await openModal<string>({
|
||||
title: "Choose a product",
|
||||
content: ({ close }) => (
|
||||
<ProductPicker onSelect={(product) => close(product.id)} />
|
||||
),
|
||||
})
|
||||
```
|
||||
|
||||
## Queues and Hosts {#queue}
|
||||
|
||||
Within one Scope, each feature has a lazily created channel and Base UI handle. Calls of the same kind enter a FIFO queue and the next request appears after the previous closing animation completes. Different features can remain visible together, so a Modal may open a Confirm.
|
||||
|
||||
Unmounting a Host settles its pending requests with their cancellation values. React Strict Mode's simulated unmount does not incorrectly cancel requests.
|
||||
|
||||
## Dismissal {#dismissal}
|
||||
|
||||
Custom content can call `dismiss()`. The close button, Escape key, and backdrop press enter the same cancellation path. Setting `dismissible: false` blocks external dismissal, while the content may still explicitly call `close(result)` or `dismiss()`.
|
||||
|
||||
## Route interception {#navigation}
|
||||
|
||||
`@workspace/overlays/navigation` depends only on Scope, not on a concrete overlay feature or router:
|
||||
|
||||
```tsx
|
||||
import { OverlayNavigationGuard } from "@workspace/overlays/navigation"
|
||||
|
||||
const adapter: OverlayNavigationAdapter = {
|
||||
subscribe(listener) {
|
||||
return router.block((transition) => {
|
||||
listener({
|
||||
cancel: transition.reset,
|
||||
proceed: transition.retry,
|
||||
})
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
<OverlayNavigationGuard adapter={adapter} />
|
||||
```
|
||||
|
||||
The default Guard examines the current and named local Scopes and finds the last-opened layer. It dismisses an allowed layer and cancels navigation. A layer with `dismissible: false` remains open and keeps blocking. It calls `proceed()` only when no layer is open.
|
||||
|
||||
The adapter's `proceed()` must continue the exact blocked attempt instead of starting a new navigation that triggers the blocker again. Pass a custom `interceptor` to replace the policy.
|
||||
@@ -0,0 +1,73 @@
|
||||
---
|
||||
title: Custom overlay examples
|
||||
description: Build result-producing modals, sheets, and drawers with UI components.
|
||||
order: 30
|
||||
toc:
|
||||
- id: typed-modal
|
||||
title: Typed modal
|
||||
- id: sheet
|
||||
title: Sheet
|
||||
- id: drawer
|
||||
title: Drawer
|
||||
---
|
||||
|
||||
## Typed modal {#typed-modal}
|
||||
|
||||
The render callback receives `close` and `dismiss`. `close` returns an application result while `dismiss` follows the cancellation path.
|
||||
|
||||
```tsx
|
||||
import { openModal } from "@workspace/overlays/modal"
|
||||
import { Button } from "@workspace/ui/components/button"
|
||||
|
||||
function PlanButton() {
|
||||
async function choosePlan() {
|
||||
const plan = await openModal<"free" | "pro">({
|
||||
title: "Choose a plan",
|
||||
description: "You can change this later.",
|
||||
content: ({ close, dismiss }) => (
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button variant="outline" onClick={dismiss}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={() => close("free")}>Free</Button>
|
||||
<Button onClick={() => close("pro")}>Pro</Button>
|
||||
</div>
|
||||
),
|
||||
})
|
||||
|
||||
if (plan) updatePlan(plan)
|
||||
}
|
||||
|
||||
return <Button onClick={choosePlan}>Change plan</Button>
|
||||
}
|
||||
```
|
||||
|
||||
## Sheet {#sheet}
|
||||
|
||||
```tsx
|
||||
import { openSheet } from "@workspace/overlays/sheet"
|
||||
|
||||
await openSheet({
|
||||
title: "Filters",
|
||||
side: "right",
|
||||
content: <ProductFilters />,
|
||||
})
|
||||
```
|
||||
|
||||
Use a sheet for a fixed edge panel. `side` accepts `top`, `right`, `bottom`, or `left`.
|
||||
|
||||
## Drawer {#drawer}
|
||||
|
||||
```tsx
|
||||
import { openDrawer } from "@workspace/overlays/drawer"
|
||||
|
||||
await openDrawer({
|
||||
title: "Choose a delivery time",
|
||||
content: <DeliverySlots />,
|
||||
showSwipeHandle: true,
|
||||
snapPoints: [0.5, 1],
|
||||
swipeDirection: "down",
|
||||
})
|
||||
```
|
||||
|
||||
Use a drawer for touch-oriented interaction with configurable swipe direction and Base UI snap points.
|
||||
@@ -0,0 +1,136 @@
|
||||
---
|
||||
title: Setup and basic interactions
|
||||
description: Mount atomic Hosts on demand and open overlays in global or local scopes.
|
||||
order: 10
|
||||
toc:
|
||||
- id: atomic-hosts
|
||||
title: Atomic Hosts
|
||||
- id: global-scope
|
||||
title: Default global scope
|
||||
- id: local-scopes
|
||||
title: Local and named scopes
|
||||
- id: message-overlays
|
||||
title: Message overlays
|
||||
- id: toast
|
||||
title: Toast
|
||||
---
|
||||
|
||||
## Atomic Hosts {#atomic-hosts}
|
||||
|
||||
Import the Tailwind source from the application stylesheet:
|
||||
|
||||
```css
|
||||
@import "@workspace/overlays/globals.css";
|
||||
```
|
||||
|
||||
Mount only the Hosts used by the application. This application does not import Sheet, Drawer, or Toast:
|
||||
|
||||
```tsx
|
||||
import { AlertHost } from "@workspace/overlays/alert"
|
||||
import { ConfirmHost } from "@workspace/overlays/confirm"
|
||||
import { ModalHost } from "@workspace/overlays/modal"
|
||||
|
||||
export function Root({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<>
|
||||
{children}
|
||||
<AlertHost />
|
||||
<ConfirmHost />
|
||||
<ModalHost />
|
||||
</>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
Applications that intentionally use every feature may import the convenience `OverlayHost` from `@workspace/overlays/all`. It eagerly imports every feature and is therefore not the default recommendation.
|
||||
|
||||
## Default global scope {#global-scope}
|
||||
|
||||
`openXxx()` functions and Hosts without a `scope` prop use `globalOverlayScope`. The caller does not need React context:
|
||||
|
||||
```tsx
|
||||
import { openAlert } from "@workspace/overlays/alert"
|
||||
|
||||
await openAlert({
|
||||
title: "Export complete",
|
||||
description: "The file is ready to download.",
|
||||
})
|
||||
```
|
||||
|
||||
The application must still mount the matching `AlertHost`. A request may be opened before its Host mounts; the Base UI handle renders it when the Host appears.
|
||||
|
||||
## Local and named scopes {#local-scopes}
|
||||
|
||||
Scopes are real objects rather than names in a global registry:
|
||||
|
||||
```tsx
|
||||
import {
|
||||
createOverlayScope,
|
||||
OverlayScopeProvider,
|
||||
useOverlayScope,
|
||||
} from "@workspace/overlays"
|
||||
import { ModalHost, openModal } from "@workspace/overlays/modal"
|
||||
|
||||
const editorScope = createOverlayScope({ name: "editor" })
|
||||
const pickerScope = createOverlayScope({ name: "picker" })
|
||||
|
||||
function EditorRoot({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<OverlayScopeProvider scope={editorScope} scopes={{ picker: pickerScope }}>
|
||||
{children}
|
||||
<ModalHost />
|
||||
<ModalHost scope={pickerScope} />
|
||||
</OverlayScopeProvider>
|
||||
)
|
||||
}
|
||||
|
||||
function OpenPickerButton() {
|
||||
const scope = useOverlayScope("picker")
|
||||
return (
|
||||
<button
|
||||
onClick={() =>
|
||||
openModal({ scope, title: "Choose an asset", content: <AssetPicker /> })
|
||||
}
|
||||
>
|
||||
Choose an asset
|
||||
</button>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
`<OverlayScopeProvider name="editor">` can create a local scope automatically. Non-React modules should retain and explicitly pass the object returned by `createOverlayScope()`.
|
||||
|
||||
## Message overlays {#message-overlays}
|
||||
|
||||
```tsx
|
||||
import { openConfirm } from "@workspace/overlays/confirm"
|
||||
|
||||
async function removeProject() {
|
||||
const accepted = await openConfirm({
|
||||
title: "Delete project?",
|
||||
description: "This action cannot be undone.",
|
||||
destructive: true,
|
||||
})
|
||||
|
||||
if (accepted) await deleteProject()
|
||||
}
|
||||
```
|
||||
|
||||
`openAlert()` returns `Promise<void>`, `openConfirm()` returns a boolean, and `openPrompt()` returns a string or `null` after cancellation.
|
||||
|
||||
## Toast {#toast}
|
||||
|
||||
```tsx
|
||||
import { dismissToast, openToast, updateToast } from "@workspace/overlays/toast"
|
||||
|
||||
const id = openToast({
|
||||
title: "Saved",
|
||||
description: "Your changes are synchronized.",
|
||||
type: "success",
|
||||
})
|
||||
|
||||
updateToast(id, { description: "All devices are synchronized." })
|
||||
dismissToast({ id })
|
||||
```
|
||||
|
||||
Toast is also isolated by Scope and rendered by the matching `ToastHost`.
|
||||
@@ -0,0 +1,32 @@
|
||||
---
|
||||
title: Overview
|
||||
description: Coordinate temporary application overlays through one API.
|
||||
order: 1
|
||||
toc:
|
||||
- id: package-role
|
||||
title: Package role
|
||||
- id: supported-overlays
|
||||
title: Supported overlays
|
||||
- id: design-boundary
|
||||
title: Design boundary
|
||||
---
|
||||
|
||||
## Package role {#package-role}
|
||||
|
||||
`@workspace/overlays` is an asynchronous overlay orchestration layer built on Base UI handles and `@workspace/ui`. Application code starts interactions through atomic functions such as `openAlert()` and `openModal()` instead of owning separate `open` state.
|
||||
|
||||
Every feature has its own subpath and Host. For example, `@workspace/overlays/modal` contains only the Modal implementation. An `OverlayScope` creates feature channels lazily instead of constructing every Base UI handle up front. Requests of the same kind are queued and never replace a Promise that is still pending.
|
||||
|
||||
## Supported overlays {#supported-overlays}
|
||||
|
||||
- `alert` waits until a message is acknowledged.
|
||||
- `confirm` returns `true` or `false`.
|
||||
- `prompt` returns the entered string, or `null` after cancellation.
|
||||
- `modal` displays centered custom content.
|
||||
- `sheet` displays a panel from a page edge.
|
||||
- `drawer` adds dragging, direction, and snap-point behavior.
|
||||
- `toast` reuses the Base UI toast manager for notifications, updates, and Promise states.
|
||||
|
||||
## Design boundary {#design-boundary}
|
||||
|
||||
The package owns scopes, request queues, Promise settlement, and standard structure without duplicating the underlying UI primitives. The root entry exports only scope capabilities; import concrete UI from subpaths such as `/alert`, `/modal`, and `/sheet` to preserve explicit tree-shaking boundaries.
|
||||
@@ -0,0 +1,64 @@
|
||||
---
|
||||
title: 生命周期与路由拦截
|
||||
description: 理解结果、队列、Host 生命周期以及路由库适配。
|
||||
order: 20
|
||||
toc:
|
||||
- id: result-contracts
|
||||
title: 结果约定
|
||||
- id: queue
|
||||
title: 队列与 Host
|
||||
- id: dismissal
|
||||
title: 关闭行为
|
||||
- id: navigation
|
||||
title: 路由拦截
|
||||
---
|
||||
|
||||
## 结果约定 {#result-contracts}
|
||||
|
||||
`openConfirm` 使用 `false` 表示取消,`openPrompt` 使用 `null`,自定义 Modal、Sheet 和 Drawer 使用 `undefined` 表示没有结果。`close(value)` 会根据 `openModal<Result>()` 的泛型检查结果。
|
||||
|
||||
```tsx
|
||||
import { openModal } from "@workspace/overlays/modal"
|
||||
|
||||
const productId = await openModal<string>({
|
||||
title: "选择产品",
|
||||
content: ({ close }) => (
|
||||
<ProductPicker onSelect={(product) => close(product.id)} />
|
||||
),
|
||||
})
|
||||
```
|
||||
|
||||
## 队列与 Host {#queue}
|
||||
|
||||
同一个 Scope 中,每种 feature 都有一个按需创建的 channel 和 Base UI handle。同类调用进入 FIFO 队列,前一个关闭动画结束后才显示下一个。不同 feature 可以同时显示,例如 Modal 内可以继续打开 Confirm。
|
||||
|
||||
Host 卸载时会用取消值结算它仍在等待的请求。React Strict Mode 的模拟卸载不会错误取消请求。
|
||||
|
||||
## 关闭行为 {#dismissal}
|
||||
|
||||
自定义内容可以调用 `dismiss()`。关闭按钮、Escape 和遮罩点击也进入取消路径。设置 `dismissible: false` 会阻止外部关闭,但内容仍可以显式调用 `close(result)` 或 `dismiss()`。
|
||||
|
||||
## 路由拦截 {#navigation}
|
||||
|
||||
`@workspace/overlays/navigation` 只依赖 Scope,不依赖具体 overlay feature 或路由库:
|
||||
|
||||
```tsx
|
||||
import { OverlayNavigationGuard } from "@workspace/overlays/navigation"
|
||||
|
||||
const adapter: OverlayNavigationAdapter = {
|
||||
subscribe(listener) {
|
||||
return router.block((transition) => {
|
||||
listener({
|
||||
cancel: transition.reset,
|
||||
proceed: transition.retry,
|
||||
})
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
<OverlayNavigationGuard adapter={adapter} />
|
||||
```
|
||||
|
||||
默认 Guard 检查当前和命名局部 Scope,找到最后打开的浮层。如果允许 dismiss,则关闭浮层并取消跳转;`dismissible: false` 会保留浮层并继续阻止跳转;没有浮层时才调用 `proceed()`。
|
||||
|
||||
适配器的 `proceed()` 必须继续已经被阻塞的同一次跳转,不能重新发起一个会再次触发 blocker 的导航。需要不同策略时,可以传入自定义 `interceptor`。
|
||||
@@ -0,0 +1,73 @@
|
||||
---
|
||||
title: 自定义浮层示例
|
||||
description: 使用 UI 组件构建有返回值的 modal、sheet 和 drawer。
|
||||
order: 30
|
||||
toc:
|
||||
- id: typed-modal
|
||||
title: 类型化 Modal
|
||||
- id: sheet
|
||||
title: Sheet
|
||||
- id: drawer
|
||||
title: Drawer
|
||||
---
|
||||
|
||||
## 类型化 Modal {#typed-modal}
|
||||
|
||||
render callback 接收 `close` 和 `dismiss`。`close` 返回业务结果,`dismiss` 进入取消路径。
|
||||
|
||||
```tsx
|
||||
import { openModal } from "@workspace/overlays/modal"
|
||||
import { Button } from "@workspace/ui/components/button"
|
||||
|
||||
function PlanButton() {
|
||||
async function choosePlan() {
|
||||
const plan = await openModal<"free" | "pro">({
|
||||
title: "选择方案",
|
||||
description: "以后仍然可以修改。",
|
||||
content: ({ close, dismiss }) => (
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button variant="outline" onClick={dismiss}>
|
||||
取消
|
||||
</Button>
|
||||
<Button onClick={() => close("free")}>免费版</Button>
|
||||
<Button onClick={() => close("pro")}>专业版</Button>
|
||||
</div>
|
||||
),
|
||||
})
|
||||
|
||||
if (plan) updatePlan(plan)
|
||||
}
|
||||
|
||||
return <Button onClick={choosePlan}>修改方案</Button>
|
||||
}
|
||||
```
|
||||
|
||||
## Sheet {#sheet}
|
||||
|
||||
```tsx
|
||||
import { openSheet } from "@workspace/overlays/sheet"
|
||||
|
||||
await openSheet({
|
||||
title: "筛选条件",
|
||||
side: "right",
|
||||
content: <ProductFilters />,
|
||||
})
|
||||
```
|
||||
|
||||
Sheet 适合固定边缘面板。`side` 支持 `top`、`right`、`bottom` 和 `left`。
|
||||
|
||||
## Drawer {#drawer}
|
||||
|
||||
```tsx
|
||||
import { openDrawer } from "@workspace/overlays/drawer"
|
||||
|
||||
await openDrawer({
|
||||
title: "选择配送时间",
|
||||
content: <DeliverySlots />,
|
||||
showSwipeHandle: true,
|
||||
snapPoints: [0.5, 1],
|
||||
swipeDirection: "down",
|
||||
})
|
||||
```
|
||||
|
||||
Drawer 适合触摸交互,可以配置滑动方向和 Base UI snap points。
|
||||
@@ -0,0 +1,136 @@
|
||||
---
|
||||
title: 安装与基础交互
|
||||
description: 按需挂载原子 Host,并使用全局或局部 Scope 打开浮层。
|
||||
order: 10
|
||||
toc:
|
||||
- id: atomic-hosts
|
||||
title: 原子 Host
|
||||
- id: global-scope
|
||||
title: 默认全局 Scope
|
||||
- id: local-scopes
|
||||
title: 局部和命名局部
|
||||
- id: message-overlays
|
||||
title: 消息类浮层
|
||||
- id: toast
|
||||
title: Toast
|
||||
---
|
||||
|
||||
## 原子 Host {#atomic-hosts}
|
||||
|
||||
在应用样式入口导入 Tailwind source:
|
||||
|
||||
```css
|
||||
@import "@workspace/overlays/globals.css";
|
||||
```
|
||||
|
||||
只挂载应用实际使用的 Host。下面的应用不会引入 Sheet、Drawer 或 Toast:
|
||||
|
||||
```tsx
|
||||
import { AlertHost } from "@workspace/overlays/alert"
|
||||
import { ConfirmHost } from "@workspace/overlays/confirm"
|
||||
import { ModalHost } from "@workspace/overlays/modal"
|
||||
|
||||
export function Root({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<>
|
||||
{children}
|
||||
<AlertHost />
|
||||
<ConfirmHost />
|
||||
<ModalHost />
|
||||
</>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
确定会使用全部能力的应用也可以从 `@workspace/overlays/all` 导入便捷的 `OverlayHost`。它会主动引入所有 feature,因此不是默认推荐方式。
|
||||
|
||||
## 默认全局 Scope {#global-scope}
|
||||
|
||||
`openXxx()` 和没有 `scope` prop 的 Host 默认使用 `globalOverlayScope`。调用方不需要 React Context:
|
||||
|
||||
```tsx
|
||||
import { openAlert } from "@workspace/overlays/alert"
|
||||
|
||||
await openAlert({
|
||||
title: "导出完成",
|
||||
description: "文件已经可以下载。",
|
||||
})
|
||||
```
|
||||
|
||||
应用仍需挂载对应的 `AlertHost`。打开请求可以发生在 Host 挂载之前,Base UI handle 会在 Host 出现后渲染它。
|
||||
|
||||
## 局部和命名局部 {#local-scopes}
|
||||
|
||||
Scope 是实际对象,不使用全局字符串注册表:
|
||||
|
||||
```tsx
|
||||
import {
|
||||
createOverlayScope,
|
||||
OverlayScopeProvider,
|
||||
useOverlayScope,
|
||||
} from "@workspace/overlays"
|
||||
import { ModalHost, openModal } from "@workspace/overlays/modal"
|
||||
|
||||
const editorScope = createOverlayScope({ name: "editor" })
|
||||
const pickerScope = createOverlayScope({ name: "picker" })
|
||||
|
||||
function EditorRoot({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<OverlayScopeProvider scope={editorScope} scopes={{ picker: pickerScope }}>
|
||||
{children}
|
||||
<ModalHost />
|
||||
<ModalHost scope={pickerScope} />
|
||||
</OverlayScopeProvider>
|
||||
)
|
||||
}
|
||||
|
||||
function OpenPickerButton() {
|
||||
const scope = useOverlayScope("picker")
|
||||
return (
|
||||
<button
|
||||
onClick={() =>
|
||||
openModal({ scope, title: "选择资源", content: <AssetPicker /> })
|
||||
}
|
||||
>
|
||||
选择资源
|
||||
</button>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
`<OverlayScopeProvider name="editor">` 可以自动创建一个局部 Scope。非 React 模块应保存并显式传递 `createOverlayScope()` 返回的对象。
|
||||
|
||||
## 消息类浮层 {#message-overlays}
|
||||
|
||||
```tsx
|
||||
import { openConfirm } from "@workspace/overlays/confirm"
|
||||
|
||||
async function removeProject() {
|
||||
const accepted = await openConfirm({
|
||||
title: "删除项目?",
|
||||
description: "此操作无法撤销。",
|
||||
destructive: true,
|
||||
})
|
||||
|
||||
if (accepted) await deleteProject()
|
||||
}
|
||||
```
|
||||
|
||||
`openAlert()` 返回 `Promise<void>`;`openConfirm()` 返回布尔值;`openPrompt()` 返回字符串,并在取消时返回 `null`。
|
||||
|
||||
## Toast {#toast}
|
||||
|
||||
```tsx
|
||||
import { dismissToast, openToast, updateToast } from "@workspace/overlays/toast"
|
||||
|
||||
const id = openToast({
|
||||
title: "保存成功",
|
||||
description: "修改已经同步。",
|
||||
type: "success",
|
||||
})
|
||||
|
||||
updateToast(id, { description: "所有设备均已同步。" })
|
||||
dismissToast({ id })
|
||||
```
|
||||
|
||||
Toast 同样按 Scope 隔离,并由对应的 `ToastHost` 渲染。
|
||||
@@ -0,0 +1,32 @@
|
||||
---
|
||||
title: 概览
|
||||
description: 使用统一 API 编排应用中的临时浮层。
|
||||
order: 1
|
||||
toc:
|
||||
- id: package-role
|
||||
title: 包的职责
|
||||
- id: supported-overlays
|
||||
title: 支持的浮层
|
||||
- id: design-boundary
|
||||
title: 设计边界
|
||||
---
|
||||
|
||||
## 包的职责 {#package-role}
|
||||
|
||||
`@workspace/overlays` 是基于 Base UI handle 和 `@workspace/ui` 的异步浮层编排层。业务代码通过 `openAlert()`、`openModal()` 等原子函数发起交互,不需要分别维护 `open` 状态。
|
||||
|
||||
每种能力都使用独立子路径和 Host,例如 `@workspace/overlays/modal` 只包含 Modal 所需实现。`OverlayScope` 按需创建已经使用的 feature channel,不会预先构造所有 Base UI handle。同类请求按顺序显示,不会覆盖仍在等待的 Promise。
|
||||
|
||||
## 支持的浮层 {#supported-overlays}
|
||||
|
||||
- `alert`:等待用户确认消息。
|
||||
- `confirm`:返回 `true` 或 `false`。
|
||||
- `prompt`:返回输入字符串,取消时返回 `null`。
|
||||
- `modal`:居中的自定义内容浮层。
|
||||
- `sheet`:从页面边缘进入的面板。
|
||||
- `drawer`:支持拖动、方向和吸附点的面板。
|
||||
- `toast`:复用 Base UI toast manager 的通知、更新和 Promise 状态。
|
||||
|
||||
## 设计边界 {#design-boundary}
|
||||
|
||||
这个包负责 Scope、请求队列、Promise 结算和标准结构,不复制底层 UI primitives。根入口只导出 Scope 能力;具体 UI 从 `/alert`、`/modal`、`/sheet` 等子路径导入,以保留明确的 tree-shaking 边界。
|
||||
Reference in New Issue
Block a user