feat(builder): add dynamic blocks email and dnd packages
This commit is contained in:
1 parent
06433e4ac4
commit
0bd2a6860d
37 files changed
+3060
-1
No files matched your search
@@ -0,0 +1,340 @@
|
||||
import {
|
||||
boolean,
|
||||
choice,
|
||||
defineBlock,
|
||||
json,
|
||||
object,
|
||||
record,
|
||||
safeUrl,
|
||||
string,
|
||||
type BlockDefinition,
|
||||
type BlockNode,
|
||||
} from "@wrnexus/blocks";
|
||||
|
||||
const styles = record(json, { maxKeys: 50 }) as never;
|
||||
const label = (key: string, fallback: string) => ({ key, fallback });
|
||||
const empty = object({}) as never;
|
||||
const contentSlot = {
|
||||
label: label("blocks.slot.content", "Content"),
|
||||
accepts: { capabilities: ["content"] },
|
||||
maxChildren: 500,
|
||||
orientation: "vertical" as const,
|
||||
sortable: true,
|
||||
};
|
||||
|
||||
const html = (value: unknown) =>
|
||||
String(value ?? "")
|
||||
.replaceAll("&", "&")
|
||||
.replaceAll("<", "<")
|
||||
.replaceAll(">", ">")
|
||||
.replaceAll('"', """)
|
||||
.replaceAll("'", "'");
|
||||
const attrs = (node: BlockNode) => node.props as Record<string, unknown>;
|
||||
const emailCell = (body: string, node: BlockNode) =>
|
||||
`<tr><td data-block-id="${html(node.id)}" style="padding:0;">${body}</td></tr>`;
|
||||
|
||||
export const emailDocumentBlock = defineBlock({
|
||||
manifest: {
|
||||
type: "layout.email-document",
|
||||
version: 1,
|
||||
name: label("blocks.document.name", "Document"),
|
||||
icon: "icon-[lucide--mail]",
|
||||
category: "layout",
|
||||
capabilities: ["root"],
|
||||
props: object({
|
||||
width: string({ default: "600" }),
|
||||
background: string({ default: "#f3f4f6" }),
|
||||
}) as never,
|
||||
styles,
|
||||
slots: { content: contentSlot },
|
||||
},
|
||||
create: ({ id }) => ({
|
||||
id: id(),
|
||||
type: "layout.email-document",
|
||||
version: 1,
|
||||
props: { width: "600", background: "#f3f4f6" },
|
||||
styles: {},
|
||||
slots: { content: [] },
|
||||
}),
|
||||
renderers: {
|
||||
emailHtml: ({ node, slots }) =>
|
||||
`<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" style="border-collapse:collapse;background:${html(attrs(node).background)};"><tr><td align="center"><table role="presentation" class="wrn-email" width="${html(attrs(node).width)}" cellpadding="0" cellspacing="0" border="0" style="border-collapse:collapse;max-width:100%;width:${html(attrs(node).width)}px;">${slots.content}</table></td></tr></table>`,
|
||||
ampEmail: ({ node, slots }) =>
|
||||
`<table role="presentation" width="100%" class="email-bg"><tr><td align="center"><table role="presentation" width="${html(attrs(node).width)}" class="email-body">${slots.content}</table></td></tr></table>`,
|
||||
mjml: ({ node, slots }) =>
|
||||
`<mjml><mj-body width="${html(attrs(node).width)}px" background-color="${html(attrs(node).background)}">${slots.content}</mj-body></mjml>`,
|
||||
},
|
||||
textRenderer: ({ slots }) => slots.content,
|
||||
} satisfies BlockDefinition);
|
||||
|
||||
export const sectionBlock = defineBlock({
|
||||
manifest: {
|
||||
type: "layout.section",
|
||||
version: 1,
|
||||
name: label("blocks.section.name", "Section"),
|
||||
icon: "icon-[lucide--panel-top]",
|
||||
category: "layout",
|
||||
capabilities: ["content"],
|
||||
props: object({
|
||||
background: string({ default: "#ffffff" }),
|
||||
padding: string({ default: "24px" }),
|
||||
}) as never,
|
||||
styles,
|
||||
slots: { content: contentSlot },
|
||||
},
|
||||
create: ({ id }) => ({
|
||||
id: id(),
|
||||
type: "layout.section",
|
||||
version: 1,
|
||||
props: { background: "#ffffff", padding: "24px" },
|
||||
styles: {},
|
||||
slots: { content: [] },
|
||||
}),
|
||||
renderers: {
|
||||
emailHtml: ({ node, slots }) =>
|
||||
emailCell(
|
||||
`<table role="presentation" width="100%"><tr><td style="background:${html(attrs(node).background)};padding:${html(attrs(node).padding)};">${slots.content}</td></tr></table>`,
|
||||
node,
|
||||
),
|
||||
ampEmail: ({ node, slots }) =>
|
||||
emailCell(
|
||||
`<table role="presentation" width="100%"><tr><td class="section">${slots.content}</td></tr></table>`,
|
||||
node,
|
||||
),
|
||||
mjml: ({ node, slots }) =>
|
||||
`<mj-section background-color="${html(attrs(node).background)}" padding="${html(attrs(node).padding)}"><mj-column>${slots.content}</mj-column></mj-section>`,
|
||||
},
|
||||
textRenderer: ({ slots }) => `${slots.content}\n`,
|
||||
} satisfies BlockDefinition);
|
||||
|
||||
export const columnsBlock = defineBlock({
|
||||
manifest: {
|
||||
type: "layout.columns",
|
||||
version: 1,
|
||||
name: label("blocks.columns.name", "Columns"),
|
||||
icon: "icon-[lucide--columns-3]",
|
||||
category: "layout",
|
||||
capabilities: ["content"],
|
||||
props: empty,
|
||||
styles,
|
||||
slots: {
|
||||
columns: {
|
||||
accepts: { types: ["layout.column"] },
|
||||
minChildren: 1,
|
||||
maxChildren: 4,
|
||||
orientation: "horizontal",
|
||||
sortable: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
create: ({ id }) => ({
|
||||
id: id(),
|
||||
type: "layout.columns",
|
||||
version: 1,
|
||||
props: {},
|
||||
styles: {},
|
||||
slots: { columns: [] },
|
||||
}),
|
||||
renderers: {
|
||||
emailHtml: ({ node, slots }) =>
|
||||
emailCell(
|
||||
`<table role="presentation" width="100%"><tr class="wrn-columns">${slots.columns}</tr></table>`,
|
||||
node,
|
||||
),
|
||||
ampEmail: ({ node, slots }) =>
|
||||
emailCell(`<table role="presentation" width="100%"><tr>${slots.columns}</tr></table>`, node),
|
||||
mjml: ({ slots }) => slots.columns,
|
||||
},
|
||||
textRenderer: ({ slots }) => slots.columns,
|
||||
} satisfies BlockDefinition);
|
||||
|
||||
export const columnBlock = defineBlock({
|
||||
manifest: {
|
||||
type: "layout.column",
|
||||
version: 1,
|
||||
name: label("blocks.column.name", "Column"),
|
||||
icon: "icon-[lucide--panel-left]",
|
||||
category: "layout",
|
||||
capabilities: ["column"],
|
||||
props: object({ width: string({ default: "100%" }) }) as never,
|
||||
styles,
|
||||
slots: { content: contentSlot },
|
||||
},
|
||||
create: ({ id }) => ({
|
||||
id: id(),
|
||||
type: "layout.column",
|
||||
version: 1,
|
||||
props: { width: "100%" },
|
||||
styles: {},
|
||||
slots: { content: [] },
|
||||
}),
|
||||
renderers: {
|
||||
emailHtml: ({ node, slots }) =>
|
||||
`<td class="wrn-column" width="${html(attrs(node).width)}" valign="top">${slots.content}</td>`,
|
||||
ampEmail: ({ node, slots }) =>
|
||||
`<td width="${html(attrs(node).width)}" valign="top">${slots.content}</td>`,
|
||||
mjml: ({ node, slots }) =>
|
||||
`<mj-column width="${html(attrs(node).width)}">${slots.content}</mj-column>`,
|
||||
},
|
||||
textRenderer: ({ slots }) => slots.content,
|
||||
} satisfies BlockDefinition);
|
||||
|
||||
const leaf = (
|
||||
type: string,
|
||||
name: string,
|
||||
icon: string,
|
||||
props: never,
|
||||
renderers: BlockDefinition["renderers"],
|
||||
textRenderer: BlockDefinition["textRenderer"],
|
||||
): BlockDefinition =>
|
||||
defineBlock({
|
||||
manifest: {
|
||||
type,
|
||||
version: 1,
|
||||
name: label(`blocks.${type}.name`, name),
|
||||
icon,
|
||||
category: "content",
|
||||
capabilities: ["content"],
|
||||
props,
|
||||
styles,
|
||||
slots: {},
|
||||
},
|
||||
create: ({ id }) => ({ id: id(), type, version: 1, props: {}, styles: {}, slots: {} }),
|
||||
renderers,
|
||||
textRenderer,
|
||||
});
|
||||
|
||||
export const headingBlock = leaf(
|
||||
"content.heading",
|
||||
"Heading",
|
||||
"icon-[lucide--heading]",
|
||||
object({
|
||||
text: string({ min: 1, max: 500, default: "Heading" }),
|
||||
level: choice(["1", "2", "3", "4"] as const, { default: "2" }),
|
||||
}) as never,
|
||||
{
|
||||
emailHtml: ({ node }) =>
|
||||
emailCell(
|
||||
`<h${attrs(node).level} style="font-family:Arial,sans-serif;margin:0 0 16px;">${html(attrs(node).text)}</h${attrs(node).level}>`,
|
||||
node,
|
||||
),
|
||||
ampEmail: ({ node }) =>
|
||||
emailCell(`<h${attrs(node).level}>${html(attrs(node).text)}</h${attrs(node).level}>`, node),
|
||||
mjml: ({ node }) =>
|
||||
`<mj-text font-size="24px" font-weight="700">${html(attrs(node).text)}</mj-text>`,
|
||||
},
|
||||
({ node }) => `${String(attrs(node).text)}\n\n`,
|
||||
);
|
||||
|
||||
export const textBlock = leaf(
|
||||
"content.text",
|
||||
"Text",
|
||||
"icon-[lucide--text]",
|
||||
object({ text: string({ max: 200_000, default: "" }) }) as never,
|
||||
{
|
||||
emailHtml: ({ node }) =>
|
||||
emailCell(
|
||||
`<p style="font-family:Arial,sans-serif;line-height:1.5;margin:0 0 16px;white-space:pre-line;">${html(attrs(node).text)}</p>`,
|
||||
node,
|
||||
),
|
||||
ampEmail: ({ node }) => emailCell(`<p>${html(attrs(node).text)}</p>`, node),
|
||||
mjml: ({ node }) => `<mj-text>${html(attrs(node).text)}</mj-text>`,
|
||||
},
|
||||
({ node }) => `${String(attrs(node).text)}\n\n`,
|
||||
);
|
||||
|
||||
export const imageBlock = leaf(
|
||||
"content.image",
|
||||
"Image",
|
||||
"icon-[lucide--image]",
|
||||
object({
|
||||
src: safeUrl({ schemes: ["https"] }),
|
||||
alt: string({ max: 1_000, default: "" }),
|
||||
width: string({ default: "600" }),
|
||||
height: string({ default: "300" }),
|
||||
decorative: boolean(false),
|
||||
}) as never,
|
||||
{
|
||||
emailHtml: ({ node }) =>
|
||||
emailCell(
|
||||
`<img src="${html(attrs(node).src)}" alt="${html(attrs(node).decorative ? "" : attrs(node).alt)}" width="${html(attrs(node).width)}" height="${html(attrs(node).height)}" style="border:0;display:block;height:auto;max-width:100%;" />`,
|
||||
node,
|
||||
),
|
||||
ampEmail: ({ node }) =>
|
||||
emailCell(
|
||||
`<amp-img src="${html(attrs(node).src)}" alt="${html(attrs(node).decorative ? "" : attrs(node).alt)}" width="${html(attrs(node).width)}" height="${html(attrs(node).height)}" layout="responsive"></amp-img>`,
|
||||
node,
|
||||
),
|
||||
mjml: ({ node }) =>
|
||||
`<mj-image src="${html(attrs(node).src)}" alt="${html(attrs(node).alt)}" width="${html(attrs(node).width)}px" />`,
|
||||
},
|
||||
({ node }) => (attrs(node).decorative ? "" : `[${String(attrs(node).alt)}]\n\n`),
|
||||
);
|
||||
|
||||
export const buttonBlock = leaf(
|
||||
"content.button",
|
||||
"Button",
|
||||
"icon-[lucide--mouse-pointer-click]",
|
||||
object({
|
||||
label: string({ min: 1, max: 200, default: "Button" }),
|
||||
href: safeUrl({ schemes: ["https", "mailto"] }),
|
||||
}) as never,
|
||||
{
|
||||
emailHtml: ({ node }) =>
|
||||
emailCell(
|
||||
`<table role="presentation"><tr><td bgcolor="#2563eb" style="border-radius:6px;"><a href="${html(attrs(node).href)}" style="color:#ffffff;display:inline-block;font-family:Arial,sans-serif;font-weight:700;padding:12px 20px;text-decoration:none;">${html(attrs(node).label)}</a></td></tr></table>`,
|
||||
node,
|
||||
),
|
||||
ampEmail: ({ node }) =>
|
||||
emailCell(
|
||||
`<a class="button" href="${html(attrs(node).href)}">${html(attrs(node).label)}</a>`,
|
||||
node,
|
||||
),
|
||||
mjml: ({ node }) =>
|
||||
`<mj-button href="${html(attrs(node).href)}">${html(attrs(node).label)}</mj-button>`,
|
||||
},
|
||||
({ node }) => `${String(attrs(node).label)}: ${String(attrs(node).href)}\n\n`,
|
||||
);
|
||||
|
||||
export const dividerBlock = leaf(
|
||||
"content.divider",
|
||||
"Divider",
|
||||
"icon-[lucide--minus]",
|
||||
empty,
|
||||
{
|
||||
emailHtml: ({ node }) =>
|
||||
emailCell(`<hr style="border:0;border-top:1px solid #d1d5db;margin:16px 0;" />`, node),
|
||||
ampEmail: ({ node }) => emailCell(`<hr />`, node),
|
||||
mjml: () => `<mj-divider />`,
|
||||
},
|
||||
() => "----------------\n",
|
||||
);
|
||||
export const spacerBlock = leaf(
|
||||
"content.spacer",
|
||||
"Spacer",
|
||||
"icon-[lucide--move-vertical]",
|
||||
object({ height: string({ default: "24" }) }) as never,
|
||||
{
|
||||
emailHtml: ({ node }) =>
|
||||
emailCell(
|
||||
`<div style="font-size:1px;height:${html(attrs(node).height)}px;line-height:${html(attrs(node).height)}px;"> </div>`,
|
||||
node,
|
||||
),
|
||||
ampEmail: ({ node }) => emailCell(`<div class="spacer"> </div>`, node),
|
||||
mjml: ({ node }) => `<mj-spacer height="${html(attrs(node).height)}px" />`,
|
||||
},
|
||||
() => "\n",
|
||||
);
|
||||
|
||||
export const emailCoreBlocks: readonly BlockDefinition[] = [
|
||||
emailDocumentBlock,
|
||||
sectionBlock,
|
||||
columnsBlock,
|
||||
columnBlock,
|
||||
headingBlock,
|
||||
textBlock,
|
||||
imageBlock,
|
||||
buttonBlock,
|
||||
dividerBlock,
|
||||
spacerBlock,
|
||||
];
|
||||
Reference in new issue
Block a user