diff --git a/.changeset/portal-shared-backdrop.md b/.changeset/portal-shared-backdrop.md new file mode 100644 index 0000000..dbf59cc --- /dev/null +++ b/.changeset/portal-shared-backdrop.md @@ -0,0 +1,5 @@ +--- +"@vlkoss/cue": minor +--- + +Portal the overlay outlet, follow close delay on the shared backdrop, and let overlay definitions opt out of or replace that dim. diff --git a/CONTEXT.md b/CONTEXT.md new file mode 100644 index 0000000..a8a3449 --- /dev/null +++ b/CONTEXT.md @@ -0,0 +1,29 @@ +# Cue + +An overlay environment for a React app. Several callers, or code outside a component, open the same overlay. The environment is not a dialog. + +## Language + +**Cue environment**: +An isolated set of overlay definitions and overlay instances that share one backdrop. +_Avoid_: overlay manager, dialog host, global overlay + +**Overlay definition**: +An overlay created from a Cue environment and opened through its handle. It may replace the shared backdrop, or opt out of providing one so a lower overlay instance can keep it. +_Avoid_: modal, dialog + +**Overlay instance**: +One opening of an overlay definition. The overlay author supplies its UI. +_Avoid_: dialog instance + +**Overlay handle**: +What callers use to open, await, and close overlay instances of one overlay definition. +_Avoid_: overlay controller, modal ref + +**Shared backdrop**: +The single dim that belongs to a Cue environment while at least one overlay instance is mounted. It is not a dialog. It comes from the top open overlay instance that did not opt out: that definition's replacement, otherwise the environment's. If every open overlay instance opted out, there is no dim. +_Avoid_: DialogOverlay, scrim, overlay + +**Close delay**: +The duration an overlay instance remains mounted after it has started to close. The shared backdrop uses the same window when that instance is the last. +_Avoid_: unmount delay, animation timeout diff --git a/README.md b/README.md index bef178b..af62408 100644 --- a/README.md +++ b/README.md @@ -65,11 +65,11 @@ confirmDialog.closeAll(); ## Using shadcn -If your app uses shadcn, the [Using Cue with shadcn guide](https://cue.vlkstudio.com/docs/shadcn) explains the registry setup and the native Dialog backdrop adapter. +If your app uses shadcn, the [Using Cue with shadcn guide](https://cue.vlkstudio.com/docs/shadcn) explains the registry setup and the shared backdrop adapter. ## Customization -Cue has no backdrop or shared components by default. Add them with `createCue({ backdrop, components })` when your application needs them. See the [Customization guide](https://cue.vlkstudio.com/docs/customization). +Cue has no backdrop or shared components by default. Add them with `createCue({ backdrop, delay, components })` when your application needs them. See the [Customization guide](https://cue.vlkstudio.com/docs/customization). ## Next.js diff --git a/apps/docs/content/docs/api.mdx b/apps/docs/content/docs/api.mdx index 1dcd2ec..30c3477 100644 --- a/apps/docs/content/docs/api.mdx +++ b/apps/docs/content/docs/api.mdx @@ -10,6 +10,7 @@ description: createCue, createOverlay, context, and the provider. ```tsx const cue = createCue({ backdrop: OverlayBackdrop, + delay: 300, components: { wrapper: OverlayWrapper, content: OverlayContent, @@ -20,9 +21,11 @@ const cue = createCue({ `components` is an application-defined map. Cue does not reserve keys such as `header` or `footer`. The exact map is inferred in every overlay callback. -`backdrop` is different. Cue renders it once for the visible stack. Individual overlays do not render it. +`backdrop` is the environment's shared backdrop. Cue renders it once for the stack. Individual overlays do not render it. -The backdrop receives a `close` callback. Call it with `{ strategy: "last" }` to close the top open overlay or `{ strategy: "all" }` to close every open overlay. +`delay` is the environment close delay in milliseconds. Default `300`. `close({ delay })` overrides it for one overlay instance. + +The backdrop receives `open` and `close`. `open` stays `true` while any overlay instance that did not opt out is open, then `false` during the last instance's close delay. Call `close` with `{ strategy: "last" }` to close the top open overlay instance or `{ strategy: "all" }` to close every open overlay instance. For the shadcn Dialog adapter, see [Using Cue with shadcn](/docs/shadcn). For custom backdrops and shared components, see [Customization](/docs/customization). @@ -32,9 +35,13 @@ For the shadcn Dialog adapter, see [Using Cue with shadcn](/docs/shadcn). For cu {children} ``` -The provider renders children, every visible overlay, and one shared backdrop while at least one instance is open. An instance remains visible while its close delay runs, but the backdrop disappears as soon as the final instance starts closing. +Mount it once, near the root. The provider portals the shared backdrop and every overlay instance to `document.body`. Pass `container` to portal somewhere else. + +A second `OverlayProvider` for the same Cue environment renders children only and warns in development. Unmounting the outlet provider dismisses pending `openAsync()` calls as `undefined` and clears the stack. + +An overlay instance remains mounted while its close delay runs. The shared backdrop stays mounted for that window when the instance is the last. -## `cue.createOverlay(render)` +## `cue.createOverlay(render, options?)` ```tsx const dialog = cue.createOverlay<{ message: string }, boolean>((props, ctx) => { @@ -61,15 +68,23 @@ type OverlayContext = { `props` contains only the object passed to `open()` or `openAsync()`. Do not expect `open`, `close`, or `components` there. +Pass `{ backdrop }` as the second argument to replace the environment dim, or `{ backdrop: false }` to opt out of providing one so a lower overlay instance can keep it: + +```tsx +const popover = cue.createOverlay(() => , { backdrop: false }); +``` + +`open()` and `openAsync()` still take application props only. + ### Handle methods -| Method | Description | -| -------------------- | ----------------------------------------------------------------------------------------------------------------------------- | -| `open(props?)` | Opens one instance and returns that instance's `close` function. Props are required when `Props` has a required key. | -| `openAsync(props?)` | Opens one instance and returns `Promise`. Dismissal resolves `undefined`. | -| `closeAll(options?)` | Closes every live instance from this overlay definition. A `result` settles every pending `openAsync()` from that definition. | +| Method | Description | +| -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `open(props?)` | Opens one instance and returns that instance's `close` function. The function accepts the same `{ result, delay }` as `ctx.close`. Props are required when `Props` has a required key. | +| `openAsync(props?)` | Opens one instance and returns `Promise`. Dismissal resolves `undefined`. | +| `closeAll(options?)` | Closes every live instance from this overlay definition. A `result` settles every pending `openAsync()` from that definition. | -Closed instances always unmount after `delay`, which defaults to `300` milliseconds. There is no `unmount: false` option. +Closed instances always unmount after `delay`. There is no `unmount: false` option. ### Optional props @@ -94,4 +109,6 @@ confirm.open({ message: "Continue?" }); ## Shared backdrop behavior -The provider renders no backdrop with no open overlays, one backdrop with one overlay, and still one backdrop for a stack. Closing the top overlay leaves the backdrop while the lower overlay remains. Closing the final instance removes it immediately, while the instance itself remains mounted until its delay expires. `closeAll()` follows the same rule. +Cue renders no dim when every open overlay instance opted out, one dim for one overlay instance, and still one dim for a stack. Closing the top overlay instance leaves the dim while a lower instance remains. Closing the last instance sets `open={false}` on the dim and removes it when that instance's close delay ends. `closeAll()` follows the same rule. + +The dim comes from the top open overlay instance that did not opt out: that overlay definition's replacement, otherwise the environment's. diff --git a/apps/docs/content/docs/customization.mdx b/apps/docs/content/docs/customization.mdx index 6a61906..026c849 100644 --- a/apps/docs/content/docs/customization.mdx +++ b/apps/docs/content/docs/customization.mdx @@ -3,35 +3,42 @@ title: Customization description: Configure a custom backdrop and reusable components for a Cue environment. --- -`createCue()` needs no options. Add a backdrop or shared components when your overlays need them. +`createCue()` needs no options. Add a backdrop, a close delay, or shared components when your overlays need them. ## Custom backdrops -Pass a component through `backdrop`: +Pass a component through `backdrop`. It is not a dialog. Cue portals it with the overlay instances. ```tsx -function OverlayBackdrop({ close }: { close: (options: { strategy: "last" | "all" }) => void }) { - return ( -