# @moveo-ai/email-templates

Transactional email templates for the Moveo.AI platform. Type-safe React Email
renderer, three locales, dark-mode support, and a white-label brand override.

## Install

```bash
npm install @moveo-ai/email-templates
# or
pnpm add @moveo-ai/email-templates
```

Peer dependency: React 18 or 19.

## Quick start

```ts
import { renderEmail } from '@moveo-ai/email-templates';

const { html, text, subject } = await renderEmail(
  'welcome',
  {
    first_name: 'Sam',
    sender_email: 'support@moveo.ai',
    console_url: 'https://console.eu.moveo.ai',
  },
  'en'
);
```

`renderEmail<Id>` is type-safe — passing the wrong `props` shape for a given
`id` is a compile error.

## Sending an email

The package is render-only — `renderEmail` returns `{ subject, html, text }`
and your service hands that to a real mail transport. SendGrid example:

```ts
import sgMail from '@sendgrid/mail';
import { renderEmail } from '@moveo-ai/email-templates';

sgMail.setApiKey(process.env.SENDGRID_API_KEY!);

const { subject, html, text } = await renderEmail(
  'welcome',
  {
    first_name: 'Sam',
    sender_email: 'support@moveo.ai',
    console_url: 'https://console.eu.moveo.ai',
  },
  'en'
);

await sgMail.send({
  from: { name: 'Moveo.AI', email: 'noreply@moveo.ai' },
  to: 'sam@example.com',
  subject,
  html,
  text,
});
```

Nodemailer, AWS SES, Postmark, Resend, Mailgun — same shape: pass `html` to
the transport's `html` field, `text` to its `text` field, and `subject` to
its `subject` field. Nothing transport-specific lives in the package.

### Per-recipient locale

For broadcasts where each recipient may want a different locale, call
`renderEmail` once per locale and send the right rendered output to each
admin. `isLocale` narrows an untrusted string to `Locale`:

```ts
import {
  DEFAULT_LOCALE,
  isLocale,
  type Locale,
  renderEmail,
} from '@moveo-ai/email-templates';

const resolveLocale = (raw: unknown): Locale =>
  isLocale(raw) ? raw : DEFAULT_LOCALE;

for (const admin of admins) {
  const locale = resolveLocale(admin.preferences?.language);
  const { subject, html, text } = await renderEmail('weekly_analytics', params, locale);
  await sgMail.send({ from, to: admin.email, subject, html, text });
}
```

Renders are deterministic for a given `(id, props, locale)` — caching per
locale is a worthwhile optimization when many recipients share one.

## Templates

| `id` | Purpose |
|---|---|
| `welcome` | Account creation. Primary CTA → console; secondary onboarding-call CTA. |
| `invitation` | Workspace invite. Differentiated CTAs for new vs existing accounts; role pills; expiration. |
| `conversation_assigned` | Hand-off notification for support agents. Channel + customer + last-message preview. |
| `request_access` | Notifies an account owner that a teammate requested a specific permission. Optional requester message + suggested-roles chip row that already grant the permission. |
| `alerts` | Real-time or digest alerts. Mixed-priority rows with deep links into the review queue. |
| `weekly_analytics` | Weekly usage digest. Metric tiles + trend chips. |
| `monthly_analytics` | Same shape as weekly, monthly cadence. |

`TEMPLATE_IDS` (a readonly array) and `isTemplateId` (a type guard) are
exported for callers that need to iterate or validate.

## White-label

Pass a `brand` object on any template's props to render under a different
brand. Every "Moveo.AI" mention switches to `brand.name`, the logo becomes
brand-name text, Moveo-specific CTAs / attribution are hidden.

```ts
await renderEmail(
  'welcome',
  {
    first_name: 'Sam',
    sender_email: 'help@acme.example.com',
    console_url: 'https://app.acme.example.com',
    brand: {
      name: 'Acme Cloud',
      company_address: '500 Market St, San Francisco, CA 94105',
      privacy_url: 'https://acme.example.com/privacy',
      terms_url: 'https://acme.example.com/terms',
    },
  },
  'en'
);
```

Only `brand.name` is required. Address / Privacy / Terms URLs are optional —
when omitted, the corresponding footer elements are hidden. Sending without
`company_address` does not meet CAN-SPAM — consumer's responsibility.

## Supported locales

`en`, `es`, `pt-br`. Locale codes are lowercase BCP-47-ish.

## Dark mode

Dark-mode rules ship in the rendered HTML and fire on:

- `@media (prefers-color-scheme: dark)` — Apple Mail (mac/iOS), Outlook 2019+
  desktop, Thunderbird.
- `[data-ogsc]` — Outlook.com webmail and Outlook for Android.

Gmail webmail ignores `prefers-color-scheme` and applies its own
auto-inversion; the palette is chosen to survive that gracefully (no
pure-black / pure-white).
