# Email Template Editor URL: /docs/web/components/email-template-editor A standalone, beehiiv-style email template editor built on PlateJS. A rich block canvas with a "+"/slash insert palette and merge-tag chips, a full Theme panel (Basic + Advanced) that live-styles every part of the email via CSS variables, email-safe HTML serialization, and a multi-provider preview simulator (Gmail / Outlook / Apple Mail / Mobile / Dark). **Demo:** ```tsx 'use client'; import { cloneDefaultTheme, EmailTemplateEditor, type EmailDocument } from '@docyrus/ui/components/email-template-editor'; /* * Rich sample that exercises the editor-renderable blocks + inlines so every * Theme-panel control has something visible to affect (headings H1-H6, marks, * links, bulleted + numbered lists, blockquote, content break, table, merge * tags). The footer preview reflects the Email Footer theme section. */ const SAMPLE_DOC: EmailDocument = { blocks: [ { type: 'h1', children: [{ text: 'Welcome, ' }, { type: 'merge_tag', name: 'first_name', children: [{ text: '' }] }, { text: '!' }] }, { type: 'p', children: [ { text: 'Your weekly product update — open the ' }, { text: 'Theme', bold: true }, { text: ' tab and watch every block restyle live, then check ' }, { text: 'Preview', bold: true }, { text: ' to see it across Gmail, Outlook, Apple Mail and mobile.' } ] }, { type: 'p', children: [ { text: 'Marks: ' }, { text: 'bold', bold: true }, { text: ', ' }, { text: 'italic', italic: true }, { text: ', ' }, { text: 'underline', underline: true }, { text: ', a ' }, { type: 'a', url: 'https://example.com', children: [{ text: 'themed link' }] }, { text: '.' } ] }, { type: 'h2', children: [{ text: 'This week' }] }, { type: 'p', indent: 1, listStyleType: 'disc', children: [{ text: 'Faster load times' }] }, { type: 'p', indent: 1, listStyleType: 'disc', children: [{ text: 'Redesigned dashboard' }] }, { type: 'p', indent: 1, listStyleType: 'disc', children: [{ text: 'New export options' }] }, { type: 'blockquote', children: [{ type: 'p', children: [{ text: 'Design is not just what it looks like. Design is how it works.' }] }] }, { type: 'hr', children: [{ text: '' }] }, { type: 'table', colSizes: [233, 233, 234], children: [ { type: 'tr', children: [{ type: 'th', children: [{ type: 'p', children: [{ text: 'Plan' }] }] }, { type: 'th', children: [{ type: 'p', children: [{ text: 'Price' }] }] }, { type: 'th', children: [{ type: 'p', children: [{ text: 'Seats' }] }] }] }, { type: 'tr', children: [{ type: 'td', children: [{ type: 'p', children: [{ text: 'Starter' }] }] }, { type: 'td', children: [{ type: 'p', children: [{ text: '$9/mo' }] }] }, { type: 'td', children: [{ type: 'p', children: [{ text: '3' }] }] }] }, { type: 'tr', children: [{ type: 'td', children: [{ type: 'p', children: [{ text: 'Pro' }] }] }, { type: 'td', children: [{ type: 'p', children: [{ text: '$29/mo' }] }] }, { type: 'td', children: [{ type: 'p', children: [{ text: '10' }] }] }] } ] }, { type: 'p', children: [{ text: 'See you next week!' }] } ] as unknown as EmailDocument['blocks'], theme: cloneDefaultTheme(), settings: { preheader: 'Your weekly update is here', subject: 'Weekly Update' } }; export function EmailTemplateEditorDemo() { return (
{/* The editor owns its own Expand button (top bar) — no external wrapper needed. */}
); } ``` ## Installation ```bash pnpm dlx @docyrus/cli add @docyrus/ui-email-template-editor ``` **Dependencies:** - [platejs](https://www.npmjs.com/package/platejs) ## Overview `EmailTemplateEditor` is a self-contained email authoring surface (no API client or auth required). The top bar has three modes plus two right-docked toggles: - **Write** — the block canvas: a clean, Notion/beehiiv-style writing column with a rich toolbar (including dedicated **Variables** and **Blocks** insert buttons), a left `+` gutter affordance, a `/` slash menu, and merge-tag chips. - **Preview** — a multi-provider simulator that renders the **email-safe HTML** across Gmail, Outlook (Word-engine approximation), Apple Mail and Mobile, with a light/dark toggle and a live size badge. - **Code** — a read-only CodeMirror view of the serialized **email-safe HTML** with an in-panel **Copy HTML** button (mirrors the HTML Template Editor's Code tab). - **Theme** (right-docked toggle, **not** a mode) — expands / collapses a right-side panel that styles every part of the email (colors, typography, spacing, borders, background, headings, links, lists, blockquote, tables, footer). Changes apply **live** to the canvas and stay open across Write / Preview / Code. - **Expand** (right-docked toggle) — the editor's own fullscreen overlay; no host wiring needed (hidden in the `dialog` variant, which is already fullscreen). The document model is a plain, serializable object — `EmailDocument = { blocks, theme, settings? }` — where `blocks` is the Plate value and `theme` is fully decoupled from the content, so persistence is a straight `JSON.stringify`. ## Usage ```tsx import { useState } from 'react'; import { cloneDefaultTheme, EmailTemplateEditor, serializeEmail, type EmailDocument } from '@docyrus/ui/components/email-template-editor'; const initial: EmailDocument = { blocks: [{ type: 'h1', children: [{ text: 'Welcome, ' }, { type: 'merge_tag', name: 'first_name', children: [{ text: '' }] }, { text: '!' }] }], theme: cloneDefaultTheme() }; export function MyEmailEditor() { const [doc, setDoc] = useState(initial); return ( console.log(serializeEmail(doc))} /> ); } ``` ### Mounting modes `variant="page"` (default) fills its container (`h-full`). `variant="dialog"` opens the same editor inside a fullscreen Radix dialog, controlled via `open` / `onOpenChange`: ```tsx const [open, setOpen] = useState(false); ``` ### Exporting HTML The **Code** mode's **Copy HTML** button (and the `onExportHtml` callback) return the full **email-safe HTML** — table-based layout, inlined CSS, a Cerberus/goodemailcode reset shell, ``, a hidden preheader, and the themed footer. Call `serializeEmail(doc)` directly to get the same string headlessly. ```tsx import { serializeEmail } from '@docyrus/ui/components/email-template-editor'; const html = serializeEmail(doc); // literal {{merge_tags}} const preview = serializeEmail(doc, { preview: true, mergeTags }); // sample values substituted ``` ## API Reference ### `EmailTemplateEditorProps` | Prop | Type | Default | Description | |------|------|---------|-------------| | `variant` | `'page' \| 'dialog'` | `'page'` | Full-page (fills container) or fullscreen dialog shell | | `open` | `boolean` | — | Dialog open state (dialog variant, controlled) | | `onOpenChange` | `(open: boolean) => void` | — | Dialog open-state change handler | | `defaultValue` | `EmailDocument` | empty doc | Uncontrolled initial document (blocks + theme) | | `value` | `EmailDocument` | — | Controlled document (optional) | | `onChange` | `(doc: EmailDocument) => void` | — | Fires on every content or theme change | | `mergeTags` | `MergeTagDef[]` | `DEFAULT_MERGE_TAGS` | Merge-tag catalog for the `{{ }}` chips + insert menu | | `onImageUpload` | `(file: File) => Promise` | — | Image upload handler (UI wired; network app-owned) | | `onExportHtml` | `(html: string) => void` | — | Fires with the email-safe HTML on **Copy HTML** (Code mode) | | `readOnly` | `boolean` | `false` | Disables editing (canvas becomes read-only) | | `title` | `string` | `'Email Template Editor'` | Dialog title (dialog variant, screen-reader label) | | `className` | `string` | — | Class applied to the editor shell | ## Type Exports | Type | Description | |------|-------------| | `EmailDocument` | `{ blocks: Value; theme: EmailTheme; settings?: { preheader?; subject? } }` — the full serializable document | | `EmailTheme` | The complete theme object (mirrors beehiiv's Basic + Advanced Style panel) | | `MergeTagDef` | `{ id: string; label: string; group?: string; sample?: string }` — a merge-tag definition | | `Align` | `'left' \| 'center' \| 'right'` | | `Spacing` | `number \| { t; r; b; l }` — all-sides or per-side spacing | | `FontWeight` | `100 … 900` | | `EmailTemplateEditorProps` | Props for the component | ### `EmailTheme` shape ```ts interface EmailTheme { colors: { outsideBackground; postBackground; textOnBackground; primary; textOnPrimary; secondary; links }; typography: { heading: { family; weight }; paragraph: { family; weight } }; spacing: { margin: number; padding: number }; borders: { cornerRadius: number; thickness: number }; background: { color: { canvas; post; postBorder }; margin; padding; radius; borderThickness }; emailHeader: { title; subtitle; image; byline; alignment; padding; code }; body: { text: { family; weight; size; lineHeight; color; gap }; headings: Record<'h1'…'h6', {...}> }; widgets: { links; buttons; breaks; lists; images; tables; blockquote }; emailFooter: { backgroundColor; textColor; family; border; margin; padding; copyright; address; alignment }; } ``` ## Exports | Export | Description | |--------|-------------| | `EmailTemplateEditor` | The component | | `serializeEmail(doc, options?)` | `EmailDocument` → full email-safe HTML string (`{ preview?, mergeTags? }`) | | `serializeEmailContent(blocks, theme)` | Serialize just the body content (no shell) | | `themeToCssVars(theme)` | `EmailTheme` → `--et-*` CSS custom-property map (canvas styling path) | | `cloneDefaultTheme()` / `DEFAULT_EMAIL_THEME` | The default beehiiv-derived theme | | `DEFAULT_MERGE_TAGS` | The default merge-tag catalog | | `CLIENT_PROFILES` | The preview render profiles (Gmail / Outlook / Apple Mail / Mobile) | | `EmailEditorProvider` / `useEmailEditorContext` | Low-level state provider + hook (advanced) | ## Blocks & inserts Blocks are inserted from the toolbar **Blocks** button (a 3-column palette), the on-canvas left **`+`** gutter (combined blocks + variables), or the **`/`** slash menu. Merge-tag variables are inserted from the toolbar **Variables** button or by typing `{{ field | fallback }}` (auto-converts to a chip). | Group | Blocks | |-------|--------| | Basics | Paragraph, Bulleted / Numbered / To-do list, Blockquote, Code block, Table, Content break (divider), Columns | | Headings | Heading 1–6 | | Dynamic | Merge tags (`{{ }}`) | | Marks | Bold, Italic, Underline, Strikethrough, Inline code, Font color, Alignment, Link | ## Theming engine One `EmailTheme`, two render targets kept in lockstep: - **Live canvas** — `themeToCssVars(theme)` produces `--et-*` CSS custom properties applied to the writing surface, so slider/color edits restyle instantly with no re-serialization. - **Email output** — `serializeEmail` folds the same theme values into **inline styles** on a table-based, client-hardened HTML document. The **Theme** panel is split into **Basic** (Colors, Typography, Spacing, Borders) and **Advanced** (Background, Email Header, Body H1–H6, Widgets, Email Footer). Basic controls write through to the Advanced fields the renderer reads, so simplified edits still change the output. ## Multi-provider preview The **Preview** mode renders the serialized email-safe HTML in a sandboxed `