Expose reusable builder kit surfaces and email preset pack
This commit is contained in:
1 parent
e019ac34c8
commit
eb1875f0bc
20 files changed
+635
-4
No files matched your search
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/email-renderer",
|
||||
"version": "0.1.44",
|
||||
"version": "0.1.45",
|
||||
"type": "module",
|
||||
"description": "WRNexus block email renderers with HTML, text, AMP Email, and MJML interoperability.",
|
||||
"exports": {
|
||||
@@ -8,6 +8,7 @@
|
||||
"./blocks": "./src/blocks.ts",
|
||||
"./browser": "./src/browser.ts",
|
||||
"./create": "./src/create.ts",
|
||||
"./presets": "./src/presets.ts",
|
||||
"./style-values": "./src/style-values.ts"
|
||||
},
|
||||
"files": [
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
import type { BlockNode } from "@wrnexus/blocks";
|
||||
import { emailCoreBlocks } from "./blocks.ts";
|
||||
|
||||
/** Complete trusted editor pack, including schema-valid content factories. */
|
||||
export const emailEditorBlocks = emailCoreBlocks.map(definition =>
|
||||
definition.manifest.category === "content" || definition.manifest.type.startsWith("commerce.")
|
||||
? { ...definition, create: (context: { id(): string }) => createEmailBlock(definition.manifest.type, context) }
|
||||
: definition,
|
||||
);
|
||||
|
||||
/** Canonical initial content for palette blocks; time and instance IDs are injectable. */
|
||||
export function createEmailBlock(type: string, context: { id(): string; now?: number }): BlockNode {
|
||||
const now = context.now ?? Date.now();
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
import type { BlockNode, JsonObject } from "@wrnexus/blocks";
|
||||
|
||||
export function createSectionPreset(name: string, id: () => string): BlockNode {
|
||||
const node = (
|
||||
type: string,
|
||||
props: JsonObject,
|
||||
slots: Record<string, BlockNode[]> = {},
|
||||
): BlockNode => ({
|
||||
id: id(),
|
||||
type,
|
||||
version: 1,
|
||||
props,
|
||||
styles: {},
|
||||
slots,
|
||||
});
|
||||
const text = (value: string) => node("content.text", { text: value });
|
||||
const heading = (value: string) => node("content.heading", { text: value, level: "2" });
|
||||
const button = (label: string, href: string) => node("content.button", { label, href });
|
||||
let content: BlockNode[];
|
||||
if (name === "welcome")
|
||||
content = [
|
||||
heading("Welcome, {{recipientName|friend}}!"),
|
||||
text("We’re glad you’re here. Discover what’s new and find your next favorite."),
|
||||
button("Explore now", "https://example.com"),
|
||||
];
|
||||
else if (name === "feature")
|
||||
content = [
|
||||
node("content.image", {
|
||||
src: "https://placehold.co/600x300",
|
||||
alt: "Replace with your featured image",
|
||||
width: "600",
|
||||
height: "300",
|
||||
decorative: false,
|
||||
}),
|
||||
heading("Something worth sharing"),
|
||||
text("Introduce your product, tell your story, or share your latest announcement."),
|
||||
button("Learn more", "https://example.com"),
|
||||
];
|
||||
else if (name === "footer")
|
||||
content = [
|
||||
node("content.divider", {}),
|
||||
text("Thank you for being part of our community."),
|
||||
text("{{organizationAddress|Add your organization address}}"),
|
||||
button("Unsubscribe", "{{unsubscribeUrl|https://example.com/unsubscribe}}"),
|
||||
];
|
||||
else throw new Error("Unknown section preset.");
|
||||
const column = node("layout.column", { width: "100%" }, { content });
|
||||
const columns = node("layout.columns", {}, { columns: [column] });
|
||||
return node("layout.section", { background: "#ffffff", padding: "24px" }, { content: [columns] });
|
||||
}
|
||||
|
||||
/** Native, editable section presets; no HTML snapshots. */
|
||||
export const emailPresets = ["welcome", "feature", "footer"].map(id => ({
|
||||
id,
|
||||
name: { key: "email.presets." + id, fallback: id[0].toUpperCase() + id.slice(1) },
|
||||
icon: "icon-[lucide--layout-template]",
|
||||
create: (context: { id(): string }) => createSectionPreset(id, context.id),
|
||||
}));
|
||||
@@ -0,0 +1,25 @@
|
||||
import { expect, test } from "bun:test";
|
||||
import { emailEditorBlocks } from "../src/create.ts";
|
||||
import { emailPresets } from "../src/presets.ts";
|
||||
|
||||
test("published email editor content factories return valid independent instances", () => {
|
||||
let id = 0;
|
||||
for (const definition of emailEditorBlocks.filter(block => block.manifest.category === "content")) {
|
||||
const first = definition.create({ id: () => String(++id) });
|
||||
const second = definition.create({ id: () => String(++id) });
|
||||
expect(first.id).not.toBe(second.id);
|
||||
expect(definition.manifest.props.parse(first.props).ok).toBe(true);
|
||||
expect(first.type).toBe(definition.manifest.type);
|
||||
}
|
||||
});
|
||||
test("published presets expose metadata and remap all instance IDs on creation", () => {
|
||||
let id = 0;
|
||||
for (const preset of emailPresets) {
|
||||
const first = preset.create({ id: () => String(++id) });
|
||||
const second = preset.create({ id: () => String(++id) });
|
||||
expect(preset.name.key).toBeTruthy();
|
||||
expect(preset.icon).toBeTruthy();
|
||||
expect(first.type).toBe("layout.section");
|
||||
expect(first.id).not.toBe(second.id);
|
||||
}
|
||||
});
|
||||
Reference in new issue
Block a user