# Email layout

The shell every transactional email uses: brand, title, body, one next step (a command to run, or a button), the ids it refers to, and why you got it. Table layout and inline styles so it survives email clients; render it to an HTML string with `renderEmail`.

- Registry name: `email-layout` (`@candor/email-layout`)
- 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/email-layout

## Install

```bash
npx candor-ui add email-layout
```

Run `npx candor-ui@latest init` once first (https://candor.themethodic.com/get-started.md). Without the CLI: `npx shadcn@latest add @candor/email-layout`.

## Example

```tsx
"use client"

import * as React from "react"
import { EmailPreview, renderEmail, type EmailNotice } from "@/components/email-layout"
import { Button } from "@/components/ui/button"

const notice: EmailNotice = {
  preheader: "Add region to customers is waiting for an approver on Acme web Production.",
  title: "An approval is waiting for you",
  paragraphs: ["Keel asked for approval of a migration to Acme web Production. You’re one of the people who can decide it.", "It expires in 7 days."],
  next: { kind: "command", text: "keel approval show apr_7f3a91" },
  refs: [
    { label: "Request", value: "apr_7f3a91" },
    { label: "Change hash", value: "sha256:9f2c4e81b7a0…" },
  ],
  why: "You’re getting this because you’re an approver on Acme web Production.",
}

export default function Example() {
  const [frame, setFrame] = React.useState(false)
  // renderEmail's string goes to your mailer. EmailPreview shows the same email inline; the iframe shows the exact document.
  const html = React.useMemo(() => renderEmail(notice, "Keel"), [])
  return (
    <div className="flex max-w-xl flex-col gap-3">
      <EmailPreview notice={notice} brand="Keel" className="border" />
      <Button size="sm" variant="outline" className="self-start" aria-expanded={frame} onClick={() => setFrame((o) => !o)}>
        {frame ? "Hide the HTML document" : "Show the HTML document"}
      </Button>
      {frame && <iframe title="Email document" srcDoc={html} sandbox="" className="block h-[30rem] w-full rounded-xl border bg-white" />}
    </div>
  )
}
```

## <EmailLayout>

Props:

- `notice: EmailNotice`: What the email says: preheader, title, body, next step, ids and why it was sent.
- `brand: string`: The product name shown at the top.
- `colors?: EmailColors | undefined` (default `defaultEmailColors`): Fixed hex colours to match your brand. Defaults to the default theme's light values.
- `strings?: Partial<typeof emailLayoutStrings> | undefined`: The email's fixed words, e.g. `{ nextRun: "Ensuite, lancez :" }`. Passed directly: emails render outside any LocaleProvider.
- `lang?: string | undefined` (default `"en"`): The email's language, for `<html lang>`.

## <EmailPreview>

The same email inline, for a settings page or the docs: no `<html>` document or iframe, its own fixed colours so it looks as it will in a mail client, and its title at a heading level that fits your page.

Props:

- `notice: EmailNotice`: What the email says.
- `brand: string`: The product name shown at the top.
- `colors?: EmailColors | undefined` (default `defaultEmailColors`): Fixed hex colours; defaults to the default theme's light values.
- `strings?: Partial<typeof emailLayoutStrings> | undefined`: The email's fixed words.
- `headingLevel?: 2 | 3 | 4 | undefined` (default `3`): The heading level of the email's title on your page. Default 3.
- `label?: string | undefined`: The preview's accessible name. Default: "Email preview: " and the title.
- `className?: string | undefined`

## Functions

### renderEmail

```ts
renderEmail(notice: EmailNotice, brand: string, colors?: EmailColors, words?: { strings?: Partial<typeof emailLayoutStrings> | undefined; lang?: string | undefined })
```

The email as a complete HTML document, ready for the mailer.

## Strings (`emailLayout`)

The fixed words, English by default. Override any of them under `emailLayout` in `<LocaleProvider strings={{ emailLayout: { … } }}>`; the table is exported as `emailLayoutStrings`.

- `nextRun`: Next, run:
- `preview`: Email preview

## Types

### EmailColors

```ts
{ ground: string; card: string; ink: string; muted: string; border: string; accent: string; code: string }
```

### EmailNextStep

```ts
| { kind: "command"; text: string } | { kind: "link"; label: string; href: string }
```

### EmailNotice

```ts
{ /** Shown in the inbox preview. */ preheader: string title: string paragraphs: string[] next?: EmailNextStep | undefined /** Ids the notice carries, e.g. a request id and a hash, rather than the data itself. */ refs?: { label: string; value: string }[] | undefined /** Why the person got this, e.g. "You can approve changes to Shop production." */ why: string }
```

## 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` (fixed strings) and `TermsProvider` (nouns); entity kinds and agent roles in `EntityKindsProvider` and `AgentRolesProvider`.
