Expose reusable builder kit surfaces and email preset pack
Quality / quality (ubuntu-latest) (push) Failing after 10m2s
Quality / quality (windows-latest) (push) Canceled after 0s

This commit is contained in:
Clintchiz committed 2026-09-05 19:51:57 +05:30
1 parent e019ac34c8
commit eb1875f0bc
20 files changed
+635 -4

No files matched your search

+2 -1
View File
@@ -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": [
+7
View File
@@ -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();
+58
View File
@@ -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);
}
});