# Toast

A portaled, auto-dismissing notification queued through a manager hook.

## Installation

```tsx
import { ToastProvider, useToast } from "@makeplane/propel/components/toast";
```
## Usage

```tsx
import { Button } from "@makeplane/propel/components/button";
import { Icon } from "@makeplane/propel/components/icon";
import { IconButton } from "@makeplane/propel/components/icon-button";
import { ToastProvider, useToast } from "@makeplane/propel/components/toast";
import { CloseOutline } from "@makeplane/propel/icons";

const closeButton = (
  <IconButton variant="ghost" size="xs" aria-label="Dismiss" icon={<Icon icon={CloseOutline} />} />
);

function ToastTrigger() {
  const { add } = useToast();
  return (
    <Button
      stretch="auto"
      variant="secondary"
      size="sm"
      label="Show notification"
      onClick={() =>
        add({
          title: "Project created",
          description: "Marketing site is ready for your team.",
          data: { variant: "success" },
        })
      }
    />
  );
}

export default function BasicDemo() {
  return (
    <ToastProvider close={closeButton}>
      <ToastTrigger />
    </ToastProvider>
  );
}
```
## Examples

### Variants

The required `variant` in a toast's `data` selects its status icon and color: `success`, `danger`, `info`, `warning`, or `neutral`.

`danger` and `warning` toasts are treated as urgent: they're announced assertively as `role="alertdialog"` (screen readers interrupt to read them immediately, instead of waiting politely) and stay open for 8 seconds instead of the usual 5 — enough time to read a failure reason before it auto-dismisses. This is automatic; no extra prop is needed. Tests that queried `getByRole("dialog")` for those variants should use `alertdialog` (often with `{ hidden: true }`, since Base UI keeps the card `aria-hidden` until the viewport is focused).

`priority` and `timeout` are derived from `variant` on `add` and when an `update` changes urgency (e.g. danger → success). An explicit value in the same call always wins; restating the same variant (progress updates) leaves an existing custom `timeout` or `priority` alone — including `timeout: 0` for persistent progress toasts.

`update` is **patch-style**: pass only the fields you mean to change (or return a partial from a functional updater). Do not spread top-level `...previous` when escalating variant — that restates stored `priority`/`timeout` as own keys and blocks derived urgency. `data` is replaced, not merged, so preserve nested fields: `update(id, (previous) => ({ title: "Failed", data: { ...previous.data, variant: "danger" } }))`. An explicit equal-to-previous `priority`/`timeout` is still honored as an intentional override.

```tsx
import { Button } from "@makeplane/propel/components/button";
import { Icon } from "@makeplane/propel/components/icon";
import { IconButton } from "@makeplane/propel/components/icon-button";
import { type ToastVariant, ToastProvider, useToast } from "@makeplane/propel/components/toast";
import { CloseOutline } from "@makeplane/propel/icons";

const closeButton = (
  <IconButton variant="ghost" size="xs" aria-label="Dismiss" icon={<Icon icon={CloseOutline} />} />
);

const VARIANTS: { variant: ToastVariant; label: string; title: string; description: string }[] = [
  {
    variant: "success",
    label: "Success",
    title: "Project created",
    description: "Marketing site is ready for your team.",
  },
  {
    variant: "danger",
    label: "Danger",
    title: "Remove failed",
    description: "Could not remove the workspace member.",
  },
  {
    variant: "info",
    label: "Info",
    title: "Import in progress",
    description: "We're syncing your issues from GitHub.",
  },
  {
    variant: "warning",
    label: "Warning",
    title: "Storage almost full",
    description: "You've used 90% of your workspace storage.",
  },
  {
    variant: "neutral",
    label: "Neutral",
    title: "Draft saved",
    description: "Your changes are stored locally.",
  },
];

function VariantTriggers() {
  const { add } = useToast();
  return (
    <div className="flex flex-wrap items-center gap-3">
      {VARIANTS.map(({ variant, label, title, description }) => (
        <Button
          key={variant}
          stretch="auto"
          variant="secondary"
          size="sm"
          label={label}
          onClick={() => add({ title, description, data: { variant } })}
        />
      ))}
    </div>
  );
}

export default function VariantsDemo() {
  return (
    <ToastProvider close={closeButton}>
      <VariantTriggers />
    </ToastProvider>
  );
}
```

### With actions

Pass `actions` for a left-aligned cluster of up to two buttons, and `primaryAction` for a right-aligned button.

```tsx
import { Button } from "@makeplane/propel/components/button";
import { Icon } from "@makeplane/propel/components/icon";
import { IconButton } from "@makeplane/propel/components/icon-button";
import { ToastProvider, useToast } from "@makeplane/propel/components/toast";
import { CloseOutline } from "@makeplane/propel/icons";

const closeButton = (
  <IconButton variant="ghost" size="xs" aria-label="Dismiss" icon={<Icon icon={CloseOutline} />} />
);

function ActionTrigger() {
  const { add } = useToast();
  return (
    <Button
      stretch="auto"
      variant="secondary"
      size="sm"
      label="Mark as done"
      onClick={() =>
        add({
          title: "Issue moved to Done",
          description: "PROJ-142 was marked complete.",
          data: {
            variant: "success",
            actions: [{ label: "Undo" }],
            primaryAction: { label: "View" },
          },
        })
      }
    />
  );
}

export default function WithActionsDemo() {
  return (
    <ToastProvider close={closeButton}>
      <ActionTrigger />
    </ToastProvider>
  );
}
```

### With progress

Set `progress` (0–100) to report a long-running task; a thin bar renders between the description and the action row.

```tsx
import { Button } from "@makeplane/propel/components/button";
import { Icon } from "@makeplane/propel/components/icon";
import { IconButton } from "@makeplane/propel/components/icon-button";
import { ToastProvider, useToast } from "@makeplane/propel/components/toast";
import { CloseOutline } from "@makeplane/propel/icons";

const closeButton = (
  <IconButton variant="ghost" size="xs" aria-label="Dismiss" icon={<Icon icon={CloseOutline} />} />
);

function ProgressTrigger() {
  const { add } = useToast();
  return (
    <Button
      stretch="auto"
      variant="secondary"
      size="sm"
      label="Export workspace"
      onClick={() =>
        add({
          title: "Exporting workspace",
          description: "Preparing your data for download.",
          data: {
            variant: "info",
            progress: 32,
            actions: [{ label: "Cancel" }],
          },
        })
      }
    />
  );
}

export default function WithProgressDemo() {
  return (
    <ToastProvider close={closeButton}>
      <ProgressTrigger />
    </ToastProvider>
  );
}
```

### Promise

`useToast().promise(promise, { loading, success, error })` queues a loading toast, then updates it in place when the promise settles.

```tsx
import { Button } from "@makeplane/propel/components/button";
import { Icon } from "@makeplane/propel/components/icon";
import { IconButton } from "@makeplane/propel/components/icon-button";
import { type ToastData, ToastProvider, useToast } from "@makeplane/propel/components/toast";
import { CloseOutline } from "@makeplane/propel/icons";

const closeButton = (
  <IconButton variant="ghost" size="xs" aria-label="Dismiss" icon={<Icon icon={CloseOutline} />} />
);

function PromiseTrigger() {
  const { promise } = useToast();
  return (
    <Button
      stretch="auto"
      variant="secondary"
      size="sm"
      label="Upload files"
      onClick={() =>
        void promise<string, ToastData>(
          new Promise<string>((resolve) => {
            setTimeout(() => resolve("3 attachments"), 1500);
          }),
          {
            loading: {
              title: "Uploading files",
              description: "Your attachments are on their way.",
              data: { variant: "neutral" },
            },
            success: (uploaded) => ({
              title: "Upload complete",
              description: `${uploaded} uploaded.`,
              data: { variant: "success" },
            }),
            error: {
              title: "Upload failed",
              description: "Something went wrong — try again.",
              data: { variant: "danger" },
            },
          },
        )
      }
    />
  );
}

export default function PromiseDemo() {
  return (
    <ToastProvider close={closeButton}>
      <PromiseTrigger />
    </ToastProvider>
  );
}
```
## API Reference

### ToastProvider

Wraps the app and renders the toast viewport. Mount it once near the root, then queue toasts with
`useToast().add({ title, description, data: { variant } })`. Composes the atomic `elements/toast`
parts (Provider + Portal + Viewport) and the manager-driven `ToastList`.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `close` (required) | `ReactElement<unknown, string \| JSXElementConstructor<any>>` | — | The close control (e.g. an `IconButton`) rendered as each toast's close button. It carries its own — localizable — `aria-label`; the toast bakes no label or glyph. |
| `toastManager` | `ToastManager<ToastData>` | — | Optional external manager. Must queue Propel `ToastData` so every toast has a `variant`. |

### Toast

A single styled toast: status icon (auto-selected from `toast.data.variant`), title, description,
optional action buttons (from `toast.data.actions` / `primaryAction`), and a close button.
Rendered automatically by `ToastProvider` for each queued toast — you normally don't render this
directly.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `close` (required) | `ReactElement<unknown, string \| JSXElementConstructor<any>>` | — | The close control (e.g. an `IconButton`), rendered as the toast's close button. It carries its own — localizable — `aria-label`; the toast bakes no label or glyph. |
| `render` | `ReactElement<unknown, string \| JSXElementConstructor<any>> \| ComponentRenderFn<HTMLProps, ToastRootState>` | — | Allows you to replace the component's HTML element with a different tag, or compose it with another component. Accepts a `ReactElement` or a function that returns the element to render. |
