feat(overlays): add managed overlay package

This commit is contained in:
Maofeng
2026-09-28 23:13:44 +08:00
parent bca874fd30
commit 0ec968c344
34 changed files with 2340 additions and 0 deletions
@@ -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`.