Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 8 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,20 +100,20 @@ renderTemplate(page, { title: 'Hello' }, { slots: { extra: '<p>More</p>' } });

The email preset calls these. They stay public so a part, or a caller, can use them directly.

| Function | Role |
| ---------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- |
| `head({ title?, preview?, styles?, extraHead? })` | `<head>` with inbox meta, `<title>`, optional preheader, styles, extra HTML |
| `main({ heading?, bodyText?, ctaLabel?, ctaUrl?, innerHtml? })` | `<main>` with an `<h1>`, a paragraph, and an optional button, or raw `innerHtml` |
| `footer({ companyName?, unsubscribeUrl? })` | `<footer>` with the company name and an optional unsubscribe link |
| `body({ preheaderHtml?, mainHtml?, footerHtml?, attributes? })` | `<body>` around an optional preheader, main, and footer. Optional raw attributes. |
| `content({ content? })` | A `<div>` around raw HTML. The email shell does not call it |
| Function | Role |
| ---------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
| `head({ title?, preview?, styles?, extraHead? })` | `<head>` with inbox meta, `<title>`, optional preheader, styles, extra HTML |
| `main({ heading?, bodyText?, ctaLabel?, ctaUrl?, innerHtml? })` | `<main>` with an `<h1>`, a paragraph, and an optional button, or raw `innerHtml` |
| `footer({ companyName?, unsubscribeUrl? })` | `<footer>` with the company name and an optional unsubscribe link |
| `body({ preheaderHtml?, mainHtml?, footerHtml?, attributes? })` | `<body>` around an optional preheader, main, and footer. Optional raw attributes. |
| `content({ content? })` | A `<div>` around raw HTML. The email shell does not call it |
| `document({ headHtml?, mainHtml?, footerHtml?, doctype?, lang?, htmlAttributes?, bodyAttributes? })` | Doctype, `<html>`, head, and body. Defaults to HTML5 + `lang="en"`. Attributes accept a raw string or a structured map. |

Omitted strings render empty. The button is present only when both `ctaLabel` and `ctaUrl` are set. The unsubscribe link is present only when `unsubscribeUrl` is set.

`bodyText`, `content`, and slot strings may contain inline HTML. Callers own that markup. `escapeHtml` is exported for text and attribute positions; the shell does not call it.

Subpath exports: `@llazyemail/template-runtime-display/escape`, `/head`, `/body`, and `/main`.
Subpath exports: `@llazyemail/template-runtime-display/escape`, `/head`, `/body`, `/main`, and `/document`.

## Scripts

Expand Down
10 changes: 10 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,16 @@
"types": "./dist/main.d.cts",
"default": "./dist/main.cjs"
}
},
"./document": {
"import": {
"types": "./dist/document.d.ts",
"default": "./dist/document.js"
},
"require": {
"types": "./dist/document.d.cts",
"default": "./dist/document.cjs"
}
}
},
"files": [
Expand Down
6 changes: 2 additions & 4 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,5 @@ export type { BodyProps } from './templates/body/body';
export { content } from './templates/content/content';
export type { ContentProps } from './templates/content/content';

export { document } from './templates/document/document';
export type { DocumentProps, DocumentShellOptions } from './templates/document/document';
export { openTag, serializeAttributes } from './utils/openTag';
export type { TagAttributes } from './utils/openTag';
export { document, mergeShell, openTag, serializeAttributes } from './templates/document';
export type { DocumentProps, DocumentShellOptions, TagAttributes } from './templates/document';
18 changes: 18 additions & 0 deletions src/templates/document/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/**
* Document shell entry.
*
* Owns the outer HTML document (doctype, html attributes, body attributes)
* and the helpers that build it. Import from the package root or from
* `@llazyemail/template-runtime-display/document`.
*/

export { document } from './document';
export type { DocumentProps, DocumentShellOptions } from './document';

export { body } from '../body/body';
export type { BodyProps } from '../body/body';

export { openTag, serializeAttributes } from '../../utils/openTag';
export type { TagAttributes } from '../../utils/openTag';

export { mergeShell } from './mergeShell';
18 changes: 18 additions & 0 deletions src/templates/document/mergeShell.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import type { DocumentShellOptions } from './document';

/**
* Merge template-level shell defaults with per-render overrides.
* Defined fields on `overrides` win. Omitted fields keep the default.
* Explicit `false` / `null` for `lang` is preserved.
*/
export function mergeShell(
defaults: DocumentShellOptions | undefined,
overrides: DocumentShellOptions,
): DocumentShellOptions {
const merged: DocumentShellOptions = { ...defaults };
if (overrides.doctype !== undefined) merged.doctype = overrides.doctype;
if (overrides.lang !== undefined) merged.lang = overrides.lang;
if (overrides.htmlAttributes !== undefined) merged.htmlAttributes = overrides.htmlAttributes;
if (overrides.bodyAttributes !== undefined) merged.bodyAttributes = overrides.bodyAttributes;
return merged;
}
20 changes: 2 additions & 18 deletions src/templates/email/shell.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { displayErrors } from '../../runtime/errors';
import { document } from '../document/document';
import type { DocumentShellOptions } from '../document/document';
import { document, mergeShell } from '../document';
import type { DocumentShellOptions } from '../document';
import { hasSlot, slot } from '../../runtime/render';
import type { RenderContext, TemplatePart } from '../../runtime/types';
import type { EmailProps } from './types';
Expand All @@ -26,22 +26,6 @@ export function shellPart(
};
}

/**
* Merge template-level shell defaults with per-render props.
* Props win when both supply a value.
*/
export function mergeShell(
defaults: DocumentShellOptions | undefined,
props: EmailProps,
): DocumentShellOptions {
const merged: DocumentShellOptions = { ...defaults };
if (props.doctype !== undefined) merged.doctype = props.doctype;
if (props.lang !== undefined) merged.lang = props.lang;
if (props.htmlAttributes !== undefined) merged.htmlAttributes = props.htmlAttributes;
if (props.bodyAttributes !== undefined) merged.bodyAttributes = props.bodyAttributes;
return merged;
}

/** Place custom part HTML inside `<body>`, after `main` and before `footer`. */
export function composeEmail(
parts: Readonly<Record<string, string>>,
Expand Down
2 changes: 1 addition & 1 deletion src/templates/email/types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { TemplatePart } from '../../runtime/types';
import type { DocumentShellOptions } from '../document/document';
import type { DocumentShellOptions } from '../document';

/** Flat props for the built-in email shell. Every field is optional. */
export interface EmailProps extends DocumentShellOptions {
Expand Down
1 change: 1 addition & 0 deletions tsup.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export default defineConfig([
head: 'src/templates/head/head.ts',
body: 'src/templates/body/body.ts',
main: 'src/templates/main/main.ts',
document: 'src/templates/document/index.ts',
},
format: ['esm', 'cjs'],
clean: false,
Expand Down
Loading