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
+121
View File
@@ -0,0 +1,121 @@
import { useTranslate } from "@workspace/i18n"
import { Button } from "@workspace/ui/components/button"
import {
createDialogHandle,
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from "@workspace/ui/components/dialog"
import { XIcon } from "@workspace/ui/components/icon"
import { OverlayController, type OverlayRequest } from "./internal/controller"
import { renderOverlayContent } from "./internal/render-content"
import { useOverlayController } from "./internal/use-controller"
import { overlayMessages } from "./messages"
import { globalOverlayScope, type OverlayScope } from "./scope"
import type { ModalOptions } from "./types"
type ModalRequestOptions = Omit<ModalOptions<unknown>, "scope">
const modalChannel = Symbol("overlay.modal")
function getModalController(scope: OverlayScope) {
return scope.getChannel(modalChannel, () => {
const handle = createDialogHandle<OverlayRequest<ModalRequestOptions>>()
return new OverlayController<ModalRequestOptions, typeof handle>({
handle,
isDismissible: (options) => options.dismissible !== false,
scope,
})
})
}
export function openModal<Result = void>(options: ModalOptions<Result>) {
const { scope = globalOverlayScope, ...modalOptions } = options
return getModalController(scope).open<Result | undefined>(
modalOptions as ModalRequestOptions,
undefined
)
}
export interface ModalHostProps {
scope?: OverlayScope
}
export function ModalHost({ scope }: ModalHostProps) {
const controller = useOverlayController(scope, getModalController)
const activeOptions = controller.getActive()?.options
return (
<Dialog<OverlayRequest<ModalRequestOptions>>
disablePointerDismissal={activeOptions?.dismissible === false}
handle={controller.handle}
modal={activeOptions?.modal}
onOpenChange={(open, details) => {
if (!open) controller.handleCloseRequest(details)
}}
onOpenChangeComplete={(open) => {
if (!open) controller.handleCloseComplete()
}}
>
{({ payload }) =>
payload ? (
<ModalContent
key={payload.id}
options={payload.options}
onDismiss={controller.dismiss}
onResolve={(value) => controller.resolve(value)}
/>
) : null
}
</Dialog>
)
}
function ModalContent({
onDismiss,
onResolve,
options,
}: {
onDismiss: () => void
onResolve: (value: unknown) => void
options: ModalRequestOptions
}) {
const t = useTranslate()
const dismissible = options.dismissible !== false
return (
<DialogContent className={options.className} showCloseButton={false}>
<DialogHeader>
<DialogTitle>{options.title}</DialogTitle>
{options.description != null && (
<DialogDescription>{options.description}</DialogDescription>
)}
</DialogHeader>
{renderOverlayContent(options.content, {
close: onResolve,
dismiss: onDismiss,
})}
{dismissible && (
<DialogClose
aria-label={t(overlayMessages.close)}
onClick={onDismiss}
render={
<Button
type="button"
variant="ghost"
size="icon-sm"
className="absolute end-4 top-4"
/>
}
>
<XIcon aria-hidden="true" />
</DialogClose>
)}
</DialogContent>
)
}
export type { ModalOptions } from "./types"