` with the company name and an optional unsubscribe link |
+| `body({ preheaderHtml?, mainHtml?, footerHtml?, attributes? })` | `` around an optional preheader, main, and footer. Optional raw attributes. |
+| `content({ content? })` | A `` around raw HTML. The email shell does not call it |
| `document({ headHtml?, mainHtml?, footerHtml?, doctype?, lang?, htmlAttributes?, bodyAttributes? })` | Doctype, ``, 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
diff --git a/package.json b/package.json
index 32decc6..95c31e4 100644
--- a/package.json
+++ b/package.json
@@ -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": [
diff --git a/src/index.ts b/src/index.ts
index f0bd0f5..5e1d032 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -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';
diff --git a/src/templates/document/index.ts b/src/templates/document/index.ts
new file mode 100644
index 0000000..a6d116e
--- /dev/null
+++ b/src/templates/document/index.ts
@@ -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';
diff --git a/src/templates/document/mergeShell.ts b/src/templates/document/mergeShell.ts
new file mode 100644
index 0000000..ad5745b
--- /dev/null
+++ b/src/templates/document/mergeShell.ts
@@ -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;
+}
diff --git a/src/templates/email/shell.ts b/src/templates/email/shell.ts
index fbb76a9..0d96769 100644
--- a/src/templates/email/shell.ts
+++ b/src/templates/email/shell.ts
@@ -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';
@@ -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 ``, after `main` and before `footer`. */
export function composeEmail(
parts: Readonly>,
diff --git a/src/templates/email/types.ts b/src/templates/email/types.ts
index 2d3320f..7df6895 100644
--- a/src/templates/email/types.ts
+++ b/src/templates/email/types.ts
@@ -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 {
diff --git a/tsup.config.ts b/tsup.config.ts
index 9378610..a623bf0 100644
--- a/tsup.config.ts
+++ b/tsup.config.ts
@@ -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,