# Vyrn — Integration Guide

**Docs & live playground:** [vyrn.vercel.app](https://vyrn.vercel.app/)

```bash
npm install vyrn
```

No `transpilePackages`, no CSS import, no provider wrapper. The stylesheet is
injected by the package and `<Toaster />` already carries `'use client'`.

---

## React (Vite, CRA, Remix SPA)

```tsx
// App.tsx
import { Toaster, toast } from 'vyrn';

export default function App() {
  return (
    <>
      <Toaster position="bottom-right" richColors />
      <button onClick={() => toast.success('Saved')}>Save</button>
    </>
  );
}
```

`<Toaster />` is self-closing — it does not need to wrap your app. Put it
anywhere that stays mounted.

---

## Next.js — App Router

```tsx
// app/layout.tsx  (stays a Server Component)
import { Toaster } from 'vyrn';

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body>
        {children}
        <Toaster richColors />
      </body>
    </html>
  );
}
```

```tsx
// app/page.tsx
'use client';
import { toast } from 'vyrn';

export default function Page() {
  return <button onClick={() => toast('Hello')}>Toast</button>;
}
```

### Where `toast()` can be called

`toast()` is a browser-side call. It is fine in:

- Client Components (`'use client'`)
- event handlers and effects
- module scope of a client bundle — it queues until `<Toaster />` mounts

It is **not** available during server rendering, so you cannot call it in the
body of a Server Component or inside a Server Action to notify the browser. To
toast after a Server Action, return a value and toast from the client:

```tsx
'use client';
import { toast } from 'vyrn';
import { saveSettings } from './actions';

export function SaveButton() {
  return (
    <button
      onClick={async () => {
        const result = await saveSettings();
        if (result.ok) toast.success('Settings saved');
        else toast.error(result.message);
      }}
    >
      Save
    </button>
  );
}
```

---

## Next.js — Pages Router

```tsx
// pages/_app.tsx
import { Toaster } from 'vyrn';

export default function App({ Component, pageProps }) {
  return (
    <>
      <Component {...pageProps} />
      <Toaster />
    </>
  );
}
```

---

## Configuration

```tsx
<Toaster
  position="bottom-right"      // default 'bottom-right'
  layout="normal"              // 'stack' | 'normal' (default 'normal')
  visibleToasts={5}            // extras queue; default 5
  duration={4000}              // default 4000 ms
  theme="system"               // 'light' | 'dark' | 'system' (default 'system')
  dir="auto"                   // 'ltr' | 'rtl' | 'auto'
  richColors={false}           // true | 'minimal' | 'soft' | 'solid'
  expand={false}               // keep a stack layout expanded
  closeButton                  // default true
  showProgressBar              // default true
  color                        // semantic tint, default true
  size="md"                    // 'sm' | 'md' | 'lg'
  gap={14}
  offset="24px"
  mobileOffset="16px"
  swipeThreshold={0.35}
  hotkey={['altKey', 'KeyT']}
  pauseWhenPageIsHidden={false}
  toastOptions={{ classNames: { toast: 'my-toast' } }}
/>
```

Full prop and option tables: [README.md](./README.md).

---

## Recipes

### Undo

```tsx
toast(`"${file.name}" deleted`, {
  action: { label: 'Undo', onClick: () => restore(file) },
});
```

### Async work

```tsx
toast.promise(uploadFile(file), {
  loading: 'Uploading…',
  success: (res) => `Uploaded ${res.name}`,
  error: (err) => (err as Error).message,
});
```

### One toast for a whole flow

```tsx
const id = toast.loading('Processing…');
toast.update(id, { content: 'Almost there', progress: 60 });
toast.success('Done', { id });   // same toast — no duplicate
```

### Determinate progress

```tsx
toast(`Uploading ${pct}%`, { id: 'upload', progress: pct, duration: 0 });
```

### Inline input

```tsx
toast('What should we call it?', {
  duration: 0,
  input: {
    placeholder: 'Name',
    onSubmit: (value) => toast.success(`Saved as ${value}`),
  },
});
```

The countdown pauses while the field has focus.

### Persistent toast

```tsx
toast.error('Connection lost', { duration: Infinity, dismissible: false });
```

### Confirm before acting

```tsx
toast('Delete this project?', {
  duration: 0,
  actions: [{ label: 'Delete', style: 'danger', onClick: remove }],
  cancel: { label: 'Keep' },
});
```

### Custom styling

```tsx
<Toaster
  toastOptions={{
    classNames: {
      toast: 'rounded-2xl border-zinc-800 bg-zinc-950',
      title: 'font-semibold',
      description: 'text-zinc-400',
      actionButton: 'bg-white text-black',
    },
  }}
/>
```

Or opt out entirely and render your own markup:

```tsx
toast.custom(undefined, {
  unstyled: true,
  jsx: <div className="my-card">Fully custom</div>,
});
```

---

## Troubleshooting

**Nothing appears.** Confirm `<Toaster />` is mounted somewhere that stays
mounted, and that the component calling `toast()` is a Client Component.

**Toasts sit behind a modal.** Raise `zIndex` on `<Toaster />`.

**Dark mode isn't picked up.** Vyrn follows `prefers-color-scheme` by default.
If your app toggles a class instead, either keep using `.dark` / `[data-theme]`
on an ancestor, or pass `theme` explicitly and drive it from your theme provider.

**Styles clash with a CSS reset.** Vyrn's stylesheet is injected at the top of
`<head>`, so your own rules win at equal specificity — target
`[data-vyrn-toast]` or `.vyrn-toast`.
