# Responsive dialog

A dialog that suits the screen it opens on: a centred `Dialog` when the viewport is `breakpoint` px wide or wider (640 unless the app says otherwise), and a `Drawer` from the bottom edge when it is narrower. The form is chosen as it opens and kept while it is open, so turning a phone or resizing a window doesn't redraw what the person was doing or lose what they typed. Server-rendered, it is the centred dialog until hydration ends. Focus goes into it when it opens and back to the trigger when it closes, in either form.

Build it from its parts as you would a Dialog: `ResponsiveDialogTrigger`, then `ResponsiveDialogContent` holding a header (title and description), an optional `ResponsiveDialogBody` for content that can grow, and a footer.

`onDismissAttempt` (on the root) and `unsaved` (on the content) work in both forms, as on `Dialog` and `Drawer`: the app is asked before Escape, a press outside, the × or a drag closes it, and unsaved work is confirmed in a bar first.

- Registry name: `responsive-dialog` (`@candor/responsive-dialog`)
- Tier: Pro. Installing it needs a Pro, Premium or Team licence key: the person runs `npx candor-ui key <key>` themselves; never ask for the key in chat or write it to a file.
- Docs: https://candor.themethodic.com/docs/responsive-dialog

## Install

```bash
npx candor-ui add responsive-dialog
```

Run `npx candor-ui@latest init` once first (https://candor.themethodic.com/get-started.md). Without the CLI: `npx shadcn@latest add @candor/responsive-dialog`.
It brings in: dialog, drawer, locale, tokens, utils.

## Example

```tsx
"use client"

import * as React from "react"
import { Button } from "@/components/candor/ui/button"
import { Input } from "@/components/candor/ui/input"
import { Label } from "@/components/candor/ui/label"
import {
  ResponsiveDialog,
  ResponsiveDialogBody,
  ResponsiveDialogClose,
  ResponsiveDialogContent,
  ResponsiveDialogDescription,
  ResponsiveDialogFooter,
  ResponsiveDialogHeader,
  ResponsiveDialogTitle,
  ResponsiveDialogTrigger,
} from "@/components/candor/ui/responsive-dialog"

export default function Example() {
  const [name, setName] = React.useState("Refunds team")
  const [saved, setSaved] = React.useState("Refunds team")
  return (
    // A centred dialog from 640px wide, a drawer from the bottom below it. Narrow the window, or open it on a phone.
    <ResponsiveDialog onOpenChange={(open) => open && setName(saved)}>
      <ResponsiveDialogTrigger render={<Button variant="outline">Rename workspace</Button>} />
      <ResponsiveDialogContent>
        <ResponsiveDialogHeader>
          <ResponsiveDialogTitle>Rename workspace</ResponsiveDialogTitle>
          <ResponsiveDialogDescription>Everyone in {saved} sees the new name. Links to it keep working.</ResponsiveDialogDescription>
        </ResponsiveDialogHeader>
        <ResponsiveDialogBody>
          <div className="flex flex-col gap-2 py-1">
            <Label htmlFor="workspace-name">Name</Label>
            <Input id="workspace-name" value={name} onChange={(e) => setName(e.target.value)} />
          </div>
        </ResponsiveDialogBody>
        <ResponsiveDialogFooter>
          <ResponsiveDialogClose render={<Button variant="outline">Cancel</Button>} />
          <ResponsiveDialogClose render={<Button onClick={() => setSaved(name.trim() || saved)}>Rename</Button>} />
        </ResponsiveDialogFooter>
      </ResponsiveDialogContent>
    </ResponsiveDialog>
  )
}
```

## <ResponsiveDialog>

Props:

- `onDismissAttempt?: ((attempt: { reason: DrawerDismissReason }) => boolean | void) | undefined`: Called when the person tries to close it by Escape, a press outside, the × or (as a drawer) a drag, before it closes. Return `false` to keep it open; anything else lets it close, or shows the unsaved bar when the content has `unsaved`.
- `open?: boolean | undefined`: Whether it is open, when the app holds the state. Leave it out to let the dialog hold it.
- `defaultOpen?: boolean | undefined` (default `false`): Whether it starts open, when the dialog holds the state. Defaults to false.
- `onOpenChange?: ((open: boolean) => void) | undefined`: Called when it opens or closes.
- `breakpoint?: number | undefined` (default `640`): The viewport width, in px, from which it is a centred dialog; narrower, a drawer from the bottom. Defaults to 640.
- `children: React.ReactNode`: The trigger and the content.

## <ResponsiveDialogBody>

The part that scrolls when its content is taller than the window allows, between the header and the footer, in either form. While it overflows it takes focus and is named by the title, so a keyboard can scroll it.

No props.

Also takes the props of `React.ComponentProps<"div">`.

## <ResponsiveDialogClose>

A control that closes it; `event.preventDefault()` in its `onClick` keeps it open, as on Radix.

No props.

Also takes the props of `React.ComponentProps<typeof DialogClose>`.

## <ResponsiveDialogContent>

Its window: a centred dialog, or a drawer from the bottom, as the viewport was when it opened (`data-form` says which). The close (×) button is in the corner of both.

Props:

- `unsaved?: string | undefined`: What a dismissal would lose, in a few words ("your reply"): while set, a dismissal asks first, in a bar in the window.
- `size?: "default" | "wide" | "full" | undefined`: The dialog's width on a wide screen: "default", "wide" or "full" (see DialogContent). The drawer is the screen's width.
- `showCloseButton?: boolean | undefined` (default `true`): Shows the close (×) button in the corner. Defaults to true.
- `className?: string | undefined`: Classes for the window, merged with Candor's.
- `children: React.ReactNode`: The header, body and footer.

## <ResponsiveDialogDescription>

The sentence under the title that says what it is for, read with its name.

No props.

Also takes the props of `React.ComponentProps<typeof DialogDescription>`.

## <ResponsiveDialogFooter>

The row at the bottom for its actions.

No props.

Also takes the props of `React.ComponentProps<"div">`.

## <ResponsiveDialogHeader>

The block at the top for its title and description.

No props.

Also takes the props of `React.ComponentProps<"div">`.

## <ResponsiveDialogTitle>

Its heading, which names it to screen readers.

No props.

Also takes the props of `React.ComponentProps<typeof DialogTitle>`.

## <ResponsiveDialogTrigger>

The control that opens it; focus returns to it when it closes.

No props.

Also takes the props of `React.ComponentProps<typeof DialogTrigger>`.

## Rules for using Candor UI

- Components take plain typed data. Never pass model output as HTML or render it as a link; messages are typed blocks.
- A control that can't be used says why: wrap it in `<Locked reason="…">`, don't just disable it.
- Don't show a value you don't have: leave it out (it renders as —) rather than guess. An estimate is labelled as one.
- Nothing is pre-selected in a decision a person makes, and the agent is named from your app's data, never from model output.
- Your app's words go in `LocaleProvider`: its fixed strings in `strings`, its nouns in `nouns`; entity kinds and agent roles in `EntityKindsProvider` and `AgentRolesProvider`.
