import { useVisibility } from "@charcuterie/logic" import { playgroundParameters } from "@charcuterie/storybook-config/story-parameters" import type { Meta, StoryObj } from "@storybook/react" import type { ReactNode } from "react" import { Button } from "../Button/Button.tsx" import { Modal } from "./Modal.tsx" const meta = { title: "Components/Overlays/Modal", component: Modal, parameters: { layout: "padded" }, args: { isDismissable: true, role: "dialog", }, } satisfies Meta export default meta type Story = StoryObj /** * The base layer has no chrome, so a demo supplies its own — the * point being that `Modal` is the backdrop-plus-dismiss-plus-portal * foundation and the content is entirely the caller's. `Dialog` is * the batteries-included version of exactly this. */ const ModalDemo = ({ buttonLabel, children, label, role, }: { buttonLabel: string children?: ReactNode label: string role?: "alertdialog" | "dialog" }): ReactNode => { const { hide, isVisible, show } = useVisibility() return ( <>
{children ?? (

A bare `Modal`: a scrim, a focus trap, Escape, and an outside press. Everything you see inside is the caller's own markup.

)}
) } export const Playground: Story = { parameters: playgroundParameters, args: { "aria-label": "Read error on title 4", children: null, isVisible: false, onClose: () => {}, }, render: () => ( ), } /** * `role="alertdialog"` is the one configuration the base exposes that * `Dialog` does not surface directly — an interruption the user has * to answer, announced as an alert rather than a plain dialog. */ export const Alert: Story = { args: { "aria-label": "Confirm erase", children: null, isVisible: false, onClose: () => {}, }, render: () => (

This erases eight completed titles.

), } /** * The keyboard contract, driven. `OverlayPanel`'s * `FloatingFocusManager` traps focus and restores it to the trigger; * `useDismiss` routes Escape and an outside press through `onClose`. */ export const Interactive: Story = { args: { "aria-label": "Stop the rip?", children: null, isVisible: false, onClose: () => {}, }, render: () => (

Escape, an outside press, or the button below all land on the same `onClose`.

), }