best-email-builder 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. package/CHANGELOG.md +133 -0
  2. package/README.md +214 -19
  3. package/dist/builder.js +4 -4
  4. package/dist/{chunk-ZSUHOCCE.js → chunk-AT73Y7JA.js} +159 -83
  5. package/dist/chunk-AT73Y7JA.js.map +1 -0
  6. package/dist/chunk-HHLOE6OZ.js +1682 -0
  7. package/dist/chunk-HHLOE6OZ.js.map +1 -0
  8. package/dist/chunk-JNHFZXDL.js +1776 -0
  9. package/dist/chunk-JNHFZXDL.js.map +1 -0
  10. package/dist/chunk-MY6WPDT4.js +6110 -0
  11. package/dist/chunk-MY6WPDT4.js.map +1 -0
  12. package/dist/{chunk-JOC752I3.js → chunk-NAEGU6PR.js} +13 -4
  13. package/dist/{chunk-JOC752I3.js.map → chunk-NAEGU6PR.js.map} +1 -1
  14. package/dist/{chunk-BVOVAEEJ.js → chunk-TGDZ4NFI.js} +44 -3
  15. package/dist/chunk-TGDZ4NFI.js.map +1 -0
  16. package/dist/index.js +45 -8
  17. package/dist/index.js.map +1 -1
  18. package/dist/library.js +3 -3
  19. package/dist/server.js +544 -204
  20. package/dist/server.js.map +1 -1
  21. package/dist/styles.css +1 -1
  22. package/dist/types/blocks/shared.d.ts +20 -0
  23. package/dist/types/core/EmailEditor.d.ts +72 -19
  24. package/dist/types/core/constants.d.ts +1 -1
  25. package/dist/types/core/mjmlDefaults.d.ts +13 -0
  26. package/dist/types/core/scrollbars.d.ts +1 -1
  27. package/dist/types/defaultEditor.d.ts +4 -1
  28. package/dist/types/engine/NativeAdapter.d.ts +28 -0
  29. package/dist/types/engine/canvas/CanvasView.d.ts +88 -0
  30. package/dist/types/engine/canvas/canvasStyles.d.ts +3 -0
  31. package/dist/types/engine/canvas/icons.d.ts +14 -0
  32. package/dist/types/engine/canvas/morph.d.ts +21 -0
  33. package/dist/types/engine/canvasMjml.d.ts +30 -0
  34. package/dist/types/engine/mjml.d.ts +31 -0
  35. package/dist/types/engine/model.d.ts +130 -0
  36. package/dist/types/engine/placement.d.ts +69 -0
  37. package/dist/types/engine/richText.d.ts +15 -0
  38. package/dist/types/engine/unwrapModule.d.ts +29 -0
  39. package/dist/types/features/brand/BrandButton.d.ts +4 -0
  40. package/dist/types/features/brand/brand.d.ts +108 -0
  41. package/dist/types/features/brand/extract.d.ts +61 -0
  42. package/dist/types/features/brand/index.d.ts +2 -0
  43. package/dist/types/features/brand/messages.d.ts +13 -0
  44. package/dist/types/features/brand/useBrand.d.ts +22 -0
  45. package/dist/types/features/generate/GenerateButton.d.ts +3 -0
  46. package/dist/types/features/generate/RewriteText.d.ts +5 -0
  47. package/dist/types/features/generate/SubjectIdeas.d.ts +16 -0
  48. package/dist/types/features/generate/chip.d.ts +1 -0
  49. package/dist/types/features/generate/claude.d.ts +21 -0
  50. package/dist/types/features/generate/draft.d.ts +122 -0
  51. package/dist/types/features/generate/generator.d.ts +118 -0
  52. package/dist/types/features/generate/index.d.ts +2 -0
  53. package/dist/types/features/generate/prompt.d.ts +76 -0
  54. package/dist/types/features/generate/purposes.d.ts +48 -0
  55. package/dist/types/features/generate/rewrite.d.ts +83 -0
  56. package/dist/types/features/generate/subjects.d.ts +83 -0
  57. package/dist/types/features/preview/PreviewButton.d.ts +1 -1
  58. package/dist/types/features/preview/darkMode.d.ts +48 -0
  59. package/dist/types/index.d.ts +10 -1
  60. package/dist/types/server/brand.d.ts +32 -0
  61. package/dist/types/server/brandImport.d.ts +93 -0
  62. package/dist/types/server/compileMjml.d.ts +2 -0
  63. package/dist/types/server/handler.d.ts +25 -0
  64. package/dist/types/server/index.d.ts +11 -0
  65. package/dist/types/storage/client.d.ts +75 -3
  66. package/dist/types/ui/fields.d.ts +2 -1
  67. package/dist/types/ui/primitives/button.d.ts +1 -1
  68. package/dist/types/ui/primitives/color-picker.d.ts +2 -1
  69. package/dist/types/ui/useLayoutWidth.d.ts +1 -1
  70. package/package.json +13 -7
  71. package/dist/chunk-3M36VVXS.js +0 -665
  72. package/dist/chunk-3M36VVXS.js.map +0 -1
  73. package/dist/chunk-57CTQNTK.js +0 -56
  74. package/dist/chunk-57CTQNTK.js.map +0 -1
  75. package/dist/chunk-BVOVAEEJ.js.map +0 -1
  76. package/dist/chunk-ILRAQTHS.js +0 -3677
  77. package/dist/chunk-ILRAQTHS.js.map +0 -1
  78. package/dist/chunk-ZSUHOCCE.js.map +0 -1
  79. package/dist/types/adapters/grapesjs/GrapesJsAdapter.d.ts +0 -33
  80. package/dist/types/adapters/grapesjs/engineConfig.d.ts +0 -148
  81. package/dist/types/adapters/grapesjs/unwrapModule.d.ts +0 -18
@@ -0,0 +1,83 @@
1
+ /**
2
+ * The words of an email, as a reader sees them: the head, the styles and the
3
+ * markup left out.
4
+ * @param {string} mjml
5
+ */
6
+ export function emailWords(mjml: string): string;
7
+ /**
8
+ * The request as the model sees it, or an error when there is nothing to
9
+ * draw subjects from.
10
+ * @param {SubjectsInput} input
11
+ */
12
+ export function cleanSubjectsInput(input: SubjectsInput): {
13
+ subject: string;
14
+ preheader: string;
15
+ text: string;
16
+ language: string;
17
+ variables: {
18
+ key: string;
19
+ label: string;
20
+ }[];
21
+ avoid: string[];
22
+ };
23
+ /** @param {ReturnType<typeof cleanSubjectsInput>} request */
24
+ export function buildSubjectsPrompt({ subject, preheader, text, language, variables, avoid }: ReturnType<typeof cleanSubjectsInput>): string;
25
+ /** The subjects in a model's answer: the JSON field, else one per line. @param {string} answer */
26
+ export function parseSubjects(answer: string): any;
27
+ /**
28
+ * The usable subjects, and what is wrong with the others (for the model).
29
+ * @param {string[]} subjects @param {ReturnType<typeof cleanSubjectsInput>} request
30
+ */
31
+ export function reviewSubjects(subjects: string[], request: ReturnType<typeof cleanSubjectsInput>): {
32
+ usable: string[];
33
+ problems: string[];
34
+ };
35
+ /**
36
+ * @param {{model: import('./generator.js').EmailModel, maxAttempts?: number}} options
37
+ * @returns {(input: SubjectsInput, options?: {signal?: AbortSignal}) => Promise<{subjects: string[], attempts: number}>}
38
+ */
39
+ export function createSubjectSuggester({ model, maxAttempts }: {
40
+ model: import("./generator.js").EmailModel;
41
+ maxAttempts?: number;
42
+ }): (input: SubjectsInput, options?: {
43
+ signal?: AbortSignal;
44
+ }) => Promise<{
45
+ subjects: string[];
46
+ attempts: number;
47
+ }>;
48
+ export const SUBJECT_COUNT: 3;
49
+ export const MAX_SUBJECT_LENGTH: 150;
50
+ export const MAX_EMAIL_WORDS: 4000;
51
+ export namespace SUBJECTS_SCHEMA {
52
+ let type: string;
53
+ namespace properties {
54
+ namespace subjects {
55
+ let type_1: string;
56
+ export { type_1 as type };
57
+ export let description: string;
58
+ export namespace items {
59
+ let type_2: string;
60
+ export { type_2 as type };
61
+ }
62
+ }
63
+ }
64
+ let required: string[];
65
+ let additionalProperties: boolean;
66
+ }
67
+ export const SUBJECTS_SYSTEM_PROMPT: "You write email subject lines. You get an email (its current subject, its preview text and its words) and answer three NEW subject lines for it, in the JSON field \"subjects\".\n\nEach subject:\n- tells what the email brings, specific to its content: under 50 characters when possible, 70 at most;\n- takes its own angle: one clear and direct, one that makes the reader curious, one personal or timely;\n- is written in the language of the email;\n- invents no facts: no price, discount, date, number, name or promise the email does not contain;\n- is plain text: no HTML, no quotation marks around it, no line break;\n- has no word in capitals, at most one \"!\", no spam wording (\"free!!!\", \"act now\", \"100% guaranteed\");\n- has no emoji, unless the current subject has one;\n- differs from the current subject and from the two others, and does not repeat the preview text.\n\nMerge tags: you may personalise ONE of the three with a merge tag from the list you are given, written exactly as listed, e.g. {{contact.firstname}}. No other merge tag.";
68
+ /**
69
+ * `text`: the words of the email (`emailWords`); `language`: the one of the
70
+ * interface, used when the email has no words yet; `avoid`: ideas already
71
+ * shown ("More ideas" asks for new ones).
72
+ */
73
+ export type SubjectsInput = {
74
+ subject?: string;
75
+ preheader?: string;
76
+ text?: string;
77
+ language?: string;
78
+ variables?: {
79
+ key: string;
80
+ label?: string;
81
+ }[];
82
+ avoid?: string[];
83
+ };
@@ -2,7 +2,7 @@ export function QualityList({ quality, warnings }: {
2
2
  quality?: never[] | undefined;
3
3
  warnings?: never[] | undefined;
4
4
  }): import("react").JSX.Element;
5
- /** Sandboxed preview frame, desktop or mobile width. */
5
+ /** Sandboxed preview frame, desktop or mobile width, light or dark. */
6
6
  export function EmailPreview({ html, loading, error, device, onDeviceChange, onRefresh, hint }: {
7
7
  html: any;
8
8
  loading?: boolean | undefined;
@@ -0,0 +1,48 @@
1
+ /** @typedef {{r: number, g: number, b: number, a: number}} Rgba channels 0–255, alpha 0–1 */
2
+ /** @param {string} token @returns {Rgba|null} */
3
+ export function parseCssColor(token: string): Rgba | null;
4
+ /** WCAG relative luminance, 0 (black) to 1 (white). @param {Rgba} color */
5
+ export function luminance({ r, g, b }: Rgba): number;
6
+ /**
7
+ * One color in dark mode. `surface`: a background or a border; otherwise a
8
+ * text color. Unknown values (`transparent`, `inherit`…) come back as given.
9
+ * @param {string} token @param {'surface'|'text'} role
10
+ */
11
+ export function darkColor(token: string, role: "surface" | "text"): string;
12
+ /** The colors of a CSS text (a sheet, or a `style` attribute) in dark mode. @param {string} css */
13
+ export function darkCss(css: string): string;
14
+ /**
15
+ * A compiled email as a dark-mode app would show it.
16
+ * @param {string} html
17
+ */
18
+ export function simulateDarkMode(html: string): string;
19
+ /**
20
+ * DARK MODE, simulated — an email as the mail apps that repaint it in dark
21
+ * mode show it (Gmail and Outlook apps, Outlook.com): light backgrounds
22
+ * turned dark, dark text turned light, everything else left as it is.
23
+ *
24
+ * surfaces background and border colors (`background`, `bgcolor`,
25
+ * `border-color`…): a LIGHT one becomes dark, same hue; a dark
26
+ * or saturated one (a brand button) stays.
27
+ * text `color`: a DARK one becomes light, same hue; a light one
28
+ * (white on a button) stays.
29
+ * images untouched: a dark logo on a transparent background disappears
30
+ * here as it does in those apps — what the preview is for.
31
+ *
32
+ * Works on the compiled HTML as a string: the colors of `style` attributes,
33
+ * of `<style>` sheets and of `bgcolor`. No script, no DOM: the preview frame
34
+ * stays sandboxed.
35
+ *
36
+ * Dependency-free: runs in the browser, on the server and in unit tests.
37
+ */
38
+ /** The page behind the email, and its default text, in dark apps. */
39
+ export const DARK_PAGE: "#121212";
40
+ /**
41
+ * channels 0–255, alpha 0–1
42
+ */
43
+ export type Rgba = {
44
+ r: number;
45
+ g: number;
46
+ b: number;
47
+ a: number;
48
+ };
@@ -6,11 +6,16 @@ export { default as TemplateLibrary } from "./ui/library/TemplateLibrary.jsx";
6
6
  export { default as EmailStudio } from "./ui/EmailStudio.jsx";
7
7
  export { default as EmailBuilderBoundary } from "./ui/EmailBuilderBoundary.jsx";
8
8
  export { default as EmailCanvas } from "./ui/EmailCanvas.jsx";
9
- export { createGrapesJsAdapter } from "./adapters/grapesjs/GrapesJsAdapter.js";
10
9
  export { createRegistry } from "./core/registry.js";
10
+ export { createEmailGenerator } from "./features/generate/generator.js";
11
+ export { EmailGenerationError } from "./features/generate/draft.js";
12
+ export { emailWords } from "./features/generate/subjects.js";
13
+ export { simulateDarkMode } from "./features/preview/darkMode.js";
11
14
  export { emailBuilderFeatures, isFeatureEnabled } from "./features/index.js";
12
15
  export { UiProvider, useUi, useT } from "./ui/context.jsx";
13
16
  export { createHttpStorage, createMemoryStorage, StorageError } from "./storage/client.js";
17
+ export { createNativeAdapter, ENGINE_NAME, EMPTY_MJML } from "./engine/NativeAdapter.js";
18
+ export { parseMjml, parseMjmlFragment, serializeMjml } from "./engine/mjml.js";
14
19
  export { readDocument, createDocument, serializeDocument, EmailDocumentError } from "./core/document.js";
15
20
  export { EMAIL_BUILDER_VERSION, EMAIL_DOCUMENT_SCHEMA_VERSION, EMAIL_DEVICES } from "./core/constants.js";
16
21
  export { DEFAULT_VARIABLES, CUSTOM_GROUP, defineVariables, mergeVariables, keyFromLabel, validateCustomVariable, tokenOf } from "./features/variables/catalog.js";
@@ -18,4 +23,8 @@ export { applyVariables, applyVariablesToText, usedVariables, unknownVariables }
18
23
  export { createStarterMjml, STARTER_MJML } from "./features/templates/starter.js";
19
24
  export { TEMPLATE_GALLERY, buildGallery, withPlaceholderImages } from "./features/templates/gallery.js";
20
25
  export { isAssetPath, absoluteAssetUrls, DEFAULT_ASSETS_PATH } from "./renderer/links.js";
26
+ export { claudeEmailModel, DEFAULT_CLAUDE_MODEL } from "./features/generate/claude.js";
27
+ export { EMAIL_PURPOSES, PURPOSE_CATEGORIES } from "./features/generate/purposes.js";
28
+ export { REWRITE_ACTIONS, REWRITE_LANGUAGES } from "./features/generate/rewrite.js";
29
+ export { applyBrand, cleanBrand, brandedSender } from "./features/brand/brand.js";
21
30
  export { createTranslator, translatorFor, LOCALES } from "./i18n/index.js";
@@ -0,0 +1,32 @@
1
+ /**
2
+ * @typedef {import('../features/brand/brand.js').Brand} Brand
3
+ *
4
+ * @typedef {{
5
+ * get: () => Promise<Brand|null>,
6
+ * save: (brand: Brand & {updatedBy: string|null}) => Promise<void>,
7
+ * }} BrandRepository
8
+ */
9
+ /**
10
+ * @param {{repository?: BrandRepository|null, t?: import('../i18n/index.js').Translate}} options
11
+ */
12
+ export function createBrandService({ repository, t }: {
13
+ repository?: BrandRepository | null;
14
+ t?: import("../i18n/index.js").Translate;
15
+ }): {
16
+ /** The brand, `{}` when none was set (or no repository). */
17
+ get(): Promise<import("../features/brand/brand.js").Brand>;
18
+ /** @param {unknown} input @param {{id?: string|number, name?: string}|null} [user] */
19
+ save(input: unknown, user?: {
20
+ id?: string | number;
21
+ name?: string;
22
+ } | null): Promise<import("../features/brand/brand.js").Brand>;
23
+ };
24
+ /** @returns {BrandRepository} */
25
+ export function createMemoryBrandRepository(): BrandRepository;
26
+ export type Brand = import("../features/brand/brand.js").Brand;
27
+ export type BrandRepository = {
28
+ get: () => Promise<Brand | null>;
29
+ save: (brand: Brand & {
30
+ updatedBy: string | null;
31
+ }) => Promise<void>;
32
+ };
@@ -0,0 +1,93 @@
1
+ /**
2
+ * May the server connect to this IP address? Public unicast only.
3
+ * @param {string} address
4
+ */
5
+ export function isPublicAddress(address: string): boolean;
6
+ /**
7
+ * What the user typed, as the address of a home page: "acme.com" →
8
+ * "https://acme.com/".
9
+ * @param {unknown} input
10
+ */
11
+ export function siteUrl(input: unknown): string;
12
+ /**
13
+ * GET one address and its redirects, under the rules above.
14
+ * @param {string} address
15
+ * @param {{
16
+ * allow: (address: string) => boolean,
17
+ * allowPort?: (port: number) => boolean,
18
+ * signal: AbortSignal,
19
+ * maxBytes: number,
20
+ * truncate?: boolean,
21
+ * accept?: string,
22
+ * redirects?: number,
23
+ * }} options
24
+ * `truncate`: past `maxBytes`, keep what came (a page's head, the start of
25
+ * a stylesheet) instead of failing.
26
+ * @returns {Promise<{bytes: Buffer, url: string, contentType: string}>}
27
+ */
28
+ export function safeGet(address: string, { allow, allowPort, signal, maxBytes, truncate, accept, redirects }: {
29
+ allow: (address: string) => boolean;
30
+ allowPort?: (port: number) => boolean;
31
+ signal: AbortSignal;
32
+ maxBytes: number;
33
+ truncate?: boolean;
34
+ accept?: string;
35
+ redirects?: number;
36
+ }): Promise<{
37
+ bytes: Buffer;
38
+ url: string;
39
+ contentType: string;
40
+ }>;
41
+ /**
42
+ * @param {{
43
+ * timeoutMs?: number,
44
+ * allowAddress?: (address: string) => boolean,
45
+ * allowPort?: (port: number) => boolean,
46
+ * }} [options]
47
+ * `allowAddress`, `allowPort`: which IP addresses and ports may be reached
48
+ * (default: public addresses, ports 80 and 443). Tests reading a local
49
+ * server loosen them; nothing else should.
50
+ */
51
+ export function createBrandImporter({ timeoutMs, allowAddress, allowPort, }?: {
52
+ timeoutMs?: number;
53
+ allowAddress?: (address: string) => boolean;
54
+ allowPort?: (port: number) => boolean;
55
+ }): {
56
+ import: (input: unknown, { signal }?: {
57
+ signal?: AbortSignal;
58
+ }) => Promise<BrandImport>;
59
+ };
60
+ export namespace BRAND_IMPORT_LIMITS {
61
+ export let pageBytes: number;
62
+ export let cssBytes: number;
63
+ export { MAX_IMAGE_BYTES as logoBytes };
64
+ export let stylesheets: number;
65
+ export let logos: number;
66
+ export let redirects: number;
67
+ export let timeoutMs: number;
68
+ }
69
+ /** Why an import failed; the handler turns `code` into a message. */
70
+ export class BrandImportError extends Error {
71
+ /** @param {BrandImportCode} code @param {string} [detail] */
72
+ constructor(code: BrandImportCode, detail?: string);
73
+ code: BrandImportCode;
74
+ }
75
+ export type BrandImportCode = "BAD_URL" | "PRIVATE_ADDRESS" | "UNREACHABLE" | "TIMEOUT" | "HTTP_ERROR" | "NOT_HTML" | "TOO_MANY_REDIRECTS" | "TOO_LARGE";
76
+ /**
77
+ * `LOGO_UNSUPPORTED`: the site's logo is an SVG or a WebP, which most email
78
+ * apps do not show; `LOGO_ICON`: the site's icon was used instead.
79
+ */
80
+ export type BrandNote = "LOGO_NOT_FOUND" | "LOGO_UNSUPPORTED" | "LOGO_ICON" | "COLORS_NOT_FOUND";
81
+ export type BrandImport = {
82
+ brand: import("../features/brand/brand.js").Brand;
83
+ logo: {
84
+ bytes: Uint8Array;
85
+ width: number;
86
+ height: number;
87
+ type: string;
88
+ ext: string;
89
+ shownWidth: number;
90
+ } | null;
91
+ notes: BrandNote[];
92
+ };
93
+ import { MAX_IMAGE_BYTES } from '../validation/images.js';
@@ -1,3 +1,5 @@
1
+ /** @param {string} html */
2
+ export function withClientMetas(html: string): string;
1
3
  /** @typedef {{line?: number, message: string, tagName?: string}} CompileWarning */
2
4
  /**
3
5
  * @param {string} mjml
@@ -23,8 +23,29 @@ export type HandlerOptions = {
23
23
  assetsPath?: string;
24
24
  publicUrl?: string | null;
25
25
  transport?: import("./sendTest.js").EmailTransport | null;
26
+ generator?: {
27
+ generate: (input: import("../features/generate/generator.js").GenerateInput, options?: {
28
+ signal?: AbortSignal;
29
+ }) => Promise<import("../features/generate/generator.js").GeneratedEmail>;
30
+ rewrite?: (input: import("../features/generate/rewrite.js").RewriteInput, options?: {
31
+ signal?: AbortSignal;
32
+ }) => Promise<{
33
+ text: string;
34
+ }>;
35
+ suggestSubjects?: (input: import("../features/generate/subjects.js").SubjectsInput, options?: {
36
+ signal?: AbortSignal;
37
+ }) => Promise<{
38
+ subjects: string[];
39
+ }>;
40
+ } | null;
26
41
  variables?: import("../features/variables/catalog.js").EmailVariable[];
27
42
  variableStore?: import("./variables.js").VariableRepository | null;
43
+ brandStore?: import("./brand.js").BrandRepository | null;
44
+ brandImporter?: {
45
+ import: (url: string, options?: {
46
+ signal?: AbortSignal;
47
+ }) => Promise<import("./brandImport.js").BrandImport>;
48
+ } | null;
28
49
  locale?: string;
29
50
  messages?: Record<string, string>;
30
51
  lang?: string;
@@ -34,6 +55,10 @@ export type HandlerOptions = {
34
55
  rateLimits?: {
35
56
  testPerHour?: number;
36
57
  uploadsPerHour?: number;
58
+ generatePerHour?: number;
59
+ rewritePerHour?: number;
60
+ subjectsPerHour?: number;
61
+ brandImportsPerHour?: number;
37
62
  };
38
63
  onError?: (error: unknown, request: Request) => void;
39
64
  };
@@ -2,12 +2,23 @@ export { createEmailBuilderHandler } from "./handler.js";
2
2
  export { toNodeListener } from "./node.js";
3
3
  export { HttpError } from "./errors.js";
4
4
  export { compileMjml } from "./compileMjml.js";
5
+ export { simulateDarkMode } from "../features/preview/darkMode.js";
5
6
  export { prepareMjml } from "../renderer/prepareMjml.js";
6
7
  export { createTemplateService, createMemoryTemplateRepository, MAX_ACTIVE_VERSIONS, LIMITS } from "./templates.js";
7
8
  export { createAssetService, createMemoryAssetRepository, inspectAsset } from "./assets.js";
8
9
  export { compileTemplate, personalize, renderEmail, htmlToText } from "./render.js";
9
10
  export { sendTestEmail, MAX_TEST_RECIPIENTS } from "./sendTest.js";
11
+ export { createEmailGenerator, cleanGenerateInput, MAX_DETAILS_LENGTH } from "../features/generate/generator.js";
12
+ export { claudeEmailModel, DEFAULT_CLAUDE_MODEL } from "../features/generate/claude.js";
13
+ export { EmailGenerationError, GENERATION_ERRORS, normalizeGeneratedMjml, parseDraft, reviewDraft } from "../features/generate/draft.js";
14
+ export { EMAIL_PURPOSES, PURPOSE_CATEGORIES, purposeById, purposesByCategory } from "../features/generate/purposes.js";
15
+ export { SYSTEM_PROMPT, EMAIL_DRAFT_SCHEMA, buildUserPrompt } from "../features/generate/prompt.js";
16
+ export { REWRITE_ACTIONS, REWRITE_LANGUAGES, REWRITE_SYSTEM_PROMPT, createRewriter, reviewRewrite } from "../features/generate/rewrite.js";
17
+ export { SUBJECTS_SYSTEM_PROMPT, createSubjectSuggester, emailWords, reviewSubjects } from "../features/generate/subjects.js";
10
18
  export { createVariableService, createMemoryVariableRepository } from "./variables.js";
19
+ export { createBrandService, createMemoryBrandRepository } from "./brand.js";
20
+ export { createBrandImporter, isPublicAddress, BrandImportError, BRAND_IMPORT_LIMITS } from "./brandImport.js";
21
+ export { applyBrand, cleanBrand } from "../features/brand/brand.js";
11
22
  export { DEFAULT_VARIABLES, defineVariables, mergeVariables, tokenOf, keyFromLabel, validateCustomVariable } from "../features/variables/catalog.js";
12
23
  export { applyVariables, applyVariablesToText, mapVariables, sampleValues, usedVariables, unknownVariables } from "../features/variables/service.js";
13
24
  export { applyServiceLinks, absoluteAssetUrls, isAssetPath, DEFAULT_ASSETS_PATH } from "../renderer/links.js";
@@ -1,34 +1,61 @@
1
1
  /**
2
2
  * Client for the routes of `createEmailBuilderHandler` (`best-email-builder/server`).
3
3
  *
4
+ * `generate: true` turns on "Generate", the AI rewriting of a text and the
5
+ * subject ideas: the server must have a `generator` (otherwise it answers 501). Off by default,
6
+ * as the buttons would only fail.
7
+ *
8
+ * `brand: true` turns on the Brand button: the server must have a
9
+ * `brandStore`, and a `brandImporter` to read a brand from a website
10
+ * (`brand: {import: false}` when it has none).
11
+ *
4
12
  * @param {{
5
13
  * baseUrl?: string,
6
14
  * headers?: Record<string, string> | (() => Record<string, string>),
7
15
  * fetch?: typeof fetch,
16
+ * generate?: boolean,
17
+ * brand?: boolean|{import?: boolean},
8
18
  * }} [options]
9
19
  * @returns {TemplateStorage}
10
20
  */
11
- export function createHttpStorage({ baseUrl, headers, fetch: fetchImpl }?: {
21
+ export function createHttpStorage({ baseUrl, headers, fetch: fetchImpl, generate, brand }?: {
12
22
  baseUrl?: string;
13
23
  headers?: Record<string, string> | (() => Record<string, string>);
14
24
  fetch?: typeof fetch;
25
+ generate?: boolean;
26
+ brand?: boolean | {
27
+ import?: boolean;
28
+ };
15
29
  }): TemplateStorage;
16
30
  /**
17
31
  * In-memory storage, same contract. Documents are kept SERIALIZED.
18
32
  * `render` and `sendTest` cannot run in a browser (MJML compiles on the
19
- * server): pass implementations to enable the preview and test buttons.
33
+ * server): pass implementations to enable the preview and test buttons;
34
+ * `generate`, `rewrite` and `suggestSubjects` likewise for the AI, `importBrand` for reading a
35
+ * brand from a website. The brand itself is kept in memory (`brand`: the
36
+ * one to start with).
20
37
  *
21
38
  * @param {{
22
39
  * render?: TemplateStorage['render'],
23
40
  * sendTest?: TemplateStorage['sendTest'],
41
+ * generate?: TemplateStorage['generate'],
42
+ * rewrite?: TemplateStorage['rewrite'],
43
+ * suggestSubjects?: TemplateStorage['suggestSubjects'],
44
+ * importBrand?: TemplateStorage['importBrand'],
45
+ * brand?: Brand|null,
24
46
  * assetsPath?: string,
25
47
  * systemVariables?: EmailVariable[],
26
48
  * }} [options] `systemVariables`: keys a custom variable may not take
27
49
  * @returns {TemplateStorage & {dump: () => object}}
28
50
  */
29
- export function createMemoryStorage({ render, sendTest, assetsPath, systemVariables }?: {
51
+ export function createMemoryStorage({ render, sendTest, generate, rewrite, suggestSubjects, importBrand, brand, assetsPath, systemVariables, }?: {
30
52
  render?: TemplateStorage["render"];
31
53
  sendTest?: TemplateStorage["sendTest"];
54
+ generate?: TemplateStorage["generate"];
55
+ rewrite?: TemplateStorage["rewrite"];
56
+ suggestSubjects?: TemplateStorage["suggestSubjects"];
57
+ importBrand?: TemplateStorage["importBrand"];
58
+ brand?: Brand | null;
32
59
  assetsPath?: string;
33
60
  systemVariables?: EmailVariable[];
34
61
  }): TemplateStorage & {
@@ -74,6 +101,14 @@ export function createMemoryStorage({ render, sendTest, assetsPath, systemVariab
74
101
  *
75
102
  * @typedef {{mjml: string, subject?: string|null, preheader?: string|null}} RenderInput
76
103
  * @typedef {{html: string, warnings: {message: string}[], quality: {code: string, level: string, message: string}[]}} RenderResult
104
+ * @typedef {import('../features/generate/generator.js').GenerateInput} GenerateInput
105
+ * @typedef {import('../features/generate/generator.js').GeneratedEmail} GeneratedEmail
106
+ * @typedef {import('../features/generate/rewrite.js').RewriteInput} RewriteInput
107
+ * @typedef {import('../features/generate/subjects.js').SubjectsInput} SubjectsInput
108
+ * @typedef {import('../features/brand/brand.js').Brand} Brand
109
+ * @typedef {{brand: Brand, notes: string[]}} BrandImportResult
110
+ * `notes`: what could not be read (`LOGO_NOT_FOUND`, `LOGO_UNSUPPORTED`,
111
+ * `LOGO_ICON`, `COLORS_NOT_FOUND`).
77
112
  * @typedef {{email: string, ok: boolean, error: string|null}} TestResult
78
113
  * @typedef {{sent: number, failed: number, invalid: string[], results: TestResult[]}} SendTestResult
79
114
  *
@@ -92,6 +127,12 @@ export function createMemoryStorage({ render, sendTest, assetsPath, systemVariab
92
127
  * listVariables?: () => Promise<EmailVariable[]>,
93
128
  * saveVariable?: (variable: Partial<EmailVariable>, options?: {create?: boolean}) => Promise<EmailVariable>,
94
129
  * deleteVariable?: (key: string) => Promise<void>,
130
+ * generate?: (input: GenerateInput, options?: {signal?: AbortSignal}) => Promise<GeneratedEmail>,
131
+ * rewrite?: (input: RewriteInput, options?: {signal?: AbortSignal}) => Promise<{text: string}>,
132
+ * suggestSubjects?: (input: SubjectsInput, options?: {signal?: AbortSignal}) => Promise<{subjects: string[]}>,
133
+ * loadBrand?: () => Promise<Brand>,
134
+ * saveBrand?: (brand: Brand) => Promise<Brand>,
135
+ * importBrand?: (url: string, options?: {signal?: AbortSignal}) => Promise<BrandImportResult>,
95
136
  * }} TemplateStorage
96
137
  */
97
138
  export class StorageError extends Error {
@@ -149,6 +190,19 @@ export type RenderResult = {
149
190
  message: string;
150
191
  }[];
151
192
  };
193
+ export type GenerateInput = import("../features/generate/generator.js").GenerateInput;
194
+ export type GeneratedEmail = import("../features/generate/generator.js").GeneratedEmail;
195
+ export type RewriteInput = import("../features/generate/rewrite.js").RewriteInput;
196
+ export type SubjectsInput = import("../features/generate/subjects.js").SubjectsInput;
197
+ export type Brand = import("../features/brand/brand.js").Brand;
198
+ /**
199
+ * `notes`: what could not be read (`LOGO_NOT_FOUND`, `LOGO_UNSUPPORTED`,
200
+ * `LOGO_ICON`, `COLORS_NOT_FOUND`).
201
+ */
202
+ export type BrandImportResult = {
203
+ brand: Brand;
204
+ notes: string[];
205
+ };
152
206
  export type TestResult = {
153
207
  email: string;
154
208
  ok: boolean;
@@ -201,4 +255,22 @@ export type TemplateStorage = {
201
255
  create?: boolean;
202
256
  }) => Promise<EmailVariable>;
203
257
  deleteVariable?: (key: string) => Promise<void>;
258
+ generate?: (input: GenerateInput, options?: {
259
+ signal?: AbortSignal;
260
+ }) => Promise<GeneratedEmail>;
261
+ rewrite?: (input: RewriteInput, options?: {
262
+ signal?: AbortSignal;
263
+ }) => Promise<{
264
+ text: string;
265
+ }>;
266
+ suggestSubjects?: (input: SubjectsInput, options?: {
267
+ signal?: AbortSignal;
268
+ }) => Promise<{
269
+ subjects: string[];
270
+ }>;
271
+ loadBrand?: () => Promise<Brand>;
272
+ saveBrand?: (brand: Brand) => Promise<Brand>;
273
+ importBrand?: (url: string, options?: {
274
+ signal?: AbortSignal;
275
+ }) => Promise<BrandImportResult>;
204
276
  };
@@ -4,9 +4,10 @@ export function emailColor(value: any): string | null;
4
4
  * Field of a property. The caller's `key` (element + property) remounts it on
5
5
  * every selection change: a draft never carries over from one element to another.
6
6
  */
7
- export function PropertyField({ property, value, onCommit, idPrefix, services }: {
7
+ export function PropertyField({ property, value, fallback, onCommit, idPrefix, services }: {
8
8
  property: any;
9
9
  value: any;
10
+ fallback?: string | undefined;
10
11
  onCommit: any;
11
12
  idPrefix?: string | undefined;
12
13
  services?: null | undefined;
@@ -8,5 +8,5 @@ export function Button({ className, variant, size, asChild, type, ...props }: {
8
8
  }): import("react").JSX.Element;
9
9
  export const buttonVariants: (props?: ({
10
10
  variant?: "link" | "default" | "destructive" | "outline" | "secondary" | "ghost" | null | undefined;
11
- size?: "default" | "xs" | "sm-compact" | "sm" | "lg" | "icon" | "icon-xs" | "icon-sm-compact" | "icon-sm" | null | undefined;
11
+ size?: "default" | "icon" | "xs" | "sm-compact" | "sm" | "lg" | "icon-xs" | "icon-sm-compact" | "icon-sm" | null | undefined;
12
12
  } & import("class-variance-authority/types").ClassProp) | undefined) => string;
@@ -2,9 +2,10 @@
2
2
  * Colour field: a swatch opening a full-spectrum picker (with alpha), a hex
3
3
  * input and presets. `value` is `#RRGGBB` or `#RRGGBBAA`; `onChange(hex)`.
4
4
  */
5
- export function ColorPicker({ value, onChange, id, className }: {
5
+ export function ColorPicker({ value, onChange, id, className, placeholder }: {
6
6
  value: any;
7
7
  onChange: any;
8
8
  id: any;
9
9
  className: any;
10
+ placeholder?: string | undefined;
10
11
  }): import("react").JSX.Element;
@@ -3,7 +3,7 @@ export function layoutFor(width: number): "wide" | "medium" | "narrow";
3
3
  /** @param {{current: HTMLElement|null}} ref */
4
4
  export function useLayoutWidth(ref: {
5
5
  current: HTMLElement | null;
6
- }): "wide" | "medium" | "narrow";
6
+ }): "medium" | "wide" | "narrow";
7
7
  export namespace LAYOUT_BREAKPOINTS {
8
8
  let wide: number;
9
9
  let medium: number;
package/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "best-email-builder",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Drag-and-drop email builder for React: MJML output that renders in Outlook, Gmail and Apple Mail, with server-side rendering, versions, variables and image uploads.",
5
5
  "keywords": [
6
6
  "email",
7
7
  "email-builder",
8
8
  "mjml",
9
9
  "react",
10
- "grapesjs",
10
+ "email-template",
11
11
  "newsletter",
12
12
  "drag-and-drop",
13
13
  "email-editor"
@@ -28,9 +28,15 @@
28
28
  "types": "./dist/types/index.d.ts",
29
29
  "typesVersions": {
30
30
  "*": {
31
- "builder": ["./dist/types/builder.d.ts"],
32
- "library": ["./dist/types/library.d.ts"],
33
- "server": ["./dist/types/server/index.d.ts"]
31
+ "builder": [
32
+ "./dist/types/builder.d.ts"
33
+ ],
34
+ "library": [
35
+ "./dist/types/library.d.ts"
36
+ ],
37
+ "server": [
38
+ "./dist/types/server/index.d.ts"
39
+ ]
34
40
  }
35
41
  },
36
42
  "exports": {
@@ -92,15 +98,15 @@
92
98
  "dependencies": {
93
99
  "class-variance-authority": "^0.7.1",
94
100
  "clsx": "^2.1.1",
95
- "grapesjs": "^0.23.6",
96
- "grapesjs-mjml": "1.0.8",
97
101
  "lucide-react": "^1.55.0",
98
102
  "mjml": "^5.4.1",
103
+ "mjml-browser": "^5.4.1",
99
104
  "radix-ui": "^1.8.0",
100
105
  "react-colorful": "^5.8.1",
101
106
  "tailwind-merge": "^3.4.1"
102
107
  },
103
108
  "devDependencies": {
109
+ "@anthropic-ai/sdk": "^0.130.0",
104
110
  "@eslint/js": "^10.0.1",
105
111
  "@ladle/react": "^5.1.1",
106
112
  "@playwright/test": "^1.64.0",