best-email-builder 0.2.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 (82) hide show
  1. package/CHANGELOG.md +186 -0
  2. package/README.md +309 -12
  3. package/dist/builder.js +44 -0
  4. package/dist/builder.js.map +1 -0
  5. package/dist/chunk-AT73Y7JA.js +1427 -0
  6. package/dist/chunk-AT73Y7JA.js.map +1 -0
  7. package/dist/chunk-HHLOE6OZ.js +1682 -0
  8. package/dist/chunk-HHLOE6OZ.js.map +1 -0
  9. package/dist/chunk-JNHFZXDL.js +1776 -0
  10. package/dist/chunk-JNHFZXDL.js.map +1 -0
  11. package/dist/chunk-MY6WPDT4.js +6110 -0
  12. package/dist/chunk-MY6WPDT4.js.map +1 -0
  13. package/dist/chunk-NAEGU6PR.js +538 -0
  14. package/dist/chunk-NAEGU6PR.js.map +1 -0
  15. package/dist/chunk-TGDZ4NFI.js +114 -0
  16. package/dist/chunk-TGDZ4NFI.js.map +1 -0
  17. package/dist/index.js +124 -4866
  18. package/dist/index.js.map +1 -1
  19. package/dist/library.js +31 -0
  20. package/dist/library.js.map +1 -0
  21. package/dist/server.js +554 -210
  22. package/dist/server.js.map +1 -1
  23. package/dist/styles.css +1 -1
  24. package/dist/types/blocks/shared.d.ts +20 -0
  25. package/dist/types/builder.d.ts +8 -0
  26. package/dist/types/core/EmailEditor.d.ts +72 -19
  27. package/dist/types/core/constants.d.ts +1 -1
  28. package/dist/types/core/mjmlDefaults.d.ts +13 -0
  29. package/dist/types/core/scrollbars.d.ts +13 -0
  30. package/dist/types/defaultEditor.d.ts +11 -0
  31. package/dist/types/engine/NativeAdapter.d.ts +28 -0
  32. package/dist/types/engine/canvas/CanvasView.d.ts +88 -0
  33. package/dist/types/engine/canvas/canvasStyles.d.ts +3 -0
  34. package/dist/types/engine/canvas/icons.d.ts +14 -0
  35. package/dist/types/engine/canvas/morph.d.ts +21 -0
  36. package/dist/types/engine/canvasMjml.d.ts +30 -0
  37. package/dist/types/engine/mjml.d.ts +31 -0
  38. package/dist/types/engine/model.d.ts +130 -0
  39. package/dist/types/engine/placement.d.ts +69 -0
  40. package/dist/types/engine/richText.d.ts +15 -0
  41. package/dist/types/engine/unwrapModule.d.ts +29 -0
  42. package/dist/types/features/brand/BrandButton.d.ts +4 -0
  43. package/dist/types/features/brand/brand.d.ts +108 -0
  44. package/dist/types/features/brand/extract.d.ts +61 -0
  45. package/dist/types/features/brand/index.d.ts +2 -0
  46. package/dist/types/features/brand/messages.d.ts +13 -0
  47. package/dist/types/features/brand/useBrand.d.ts +22 -0
  48. package/dist/types/features/generate/GenerateButton.d.ts +3 -0
  49. package/dist/types/features/generate/RewriteText.d.ts +5 -0
  50. package/dist/types/features/generate/SubjectIdeas.d.ts +16 -0
  51. package/dist/types/features/generate/chip.d.ts +1 -0
  52. package/dist/types/features/generate/claude.d.ts +21 -0
  53. package/dist/types/features/generate/draft.d.ts +122 -0
  54. package/dist/types/features/generate/generator.d.ts +118 -0
  55. package/dist/types/features/generate/index.d.ts +2 -0
  56. package/dist/types/features/generate/prompt.d.ts +76 -0
  57. package/dist/types/features/generate/purposes.d.ts +48 -0
  58. package/dist/types/features/generate/rewrite.d.ts +83 -0
  59. package/dist/types/features/generate/subjects.d.ts +83 -0
  60. package/dist/types/features/preview/PreviewButton.d.ts +1 -1
  61. package/dist/types/features/preview/darkMode.d.ts +48 -0
  62. package/dist/types/features/templates/gallery.d.ts +7 -0
  63. package/dist/types/index.d.ts +14 -10
  64. package/dist/types/library.d.ts +5 -0
  65. package/dist/types/server/brand.d.ts +32 -0
  66. package/dist/types/server/brandImport.d.ts +93 -0
  67. package/dist/types/server/compileMjml.d.ts +2 -0
  68. package/dist/types/server/handler.d.ts +25 -0
  69. package/dist/types/server/index.d.ts +11 -0
  70. package/dist/types/storage/client.d.ts +79 -3
  71. package/dist/types/ui/EmailStudio.d.ts +42 -0
  72. package/dist/types/ui/fields.d.ts +2 -1
  73. package/dist/types/ui/library/TemplateLibrary.d.ts +41 -0
  74. package/dist/types/ui/primitives/button.d.ts +2 -2
  75. package/dist/types/ui/primitives/color-picker.d.ts +2 -1
  76. package/dist/types/ui/useLayoutWidth.d.ts +1 -1
  77. package/package.json +27 -4
  78. package/dist/chunk-MTALS5GC.js +0 -747
  79. package/dist/chunk-MTALS5GC.js.map +0 -1
  80. package/dist/types/adapters/grapesjs/GrapesJsAdapter.d.ts +0 -33
  81. package/dist/types/adapters/grapesjs/engineConfig.d.ts +0 -162
  82. 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
+ };
@@ -22,6 +22,13 @@ export function buildGallery({ templates, sender, t }?: {
22
22
  * @param {string} mjml
23
23
  */
24
24
  export function withThumbnailImages(mjml: string): string;
25
+ /**
26
+ * Compiled HTML for a thumbnail or a preview: an image not chosen yet (an
27
+ * `<img>` with no `src`) gets the same grey frame. For HTML that went through
28
+ * the server's validator, which rightly refuses `data:` images in MJML.
29
+ * @param {string} html
30
+ */
31
+ export function withPlaceholderImages(html: string): string;
25
32
  /** @type {GalleryTemplate[]} */
26
33
  export const TEMPLATE_GALLERY: GalleryTemplate[];
27
34
  export type StarterSender = import("./starter.js").StarterSender;
@@ -1,26 +1,30 @@
1
- /**
2
- * An editor wired to the default engine and the built-in registries.
3
- *
4
- * @param {{locale?: string}} [options] `locale`: engine UI locale (`'en'` by default)
5
- */
6
- export function createDefaultEmailEditor(options?: {
7
- locale?: string;
8
- }): import("./core/EmailEditor.js").EmailEditor;
9
1
  export { createEmailEditor } from "./core/EmailEditor.js";
10
2
  export { emailBlocks } from "./blocks/index.js";
3
+ export { createDefaultEmailEditor } from "./defaultEditor.js";
11
4
  export { default as EmailBuilder } from "./ui/EmailBuilder.jsx";
5
+ export { default as TemplateLibrary } from "./ui/library/TemplateLibrary.jsx";
6
+ export { default as EmailStudio } from "./ui/EmailStudio.jsx";
12
7
  export { default as EmailBuilderBoundary } from "./ui/EmailBuilderBoundary.jsx";
13
8
  export { default as EmailCanvas } from "./ui/EmailCanvas.jsx";
14
- export { createGrapesJsAdapter } from "./adapters/grapesjs/GrapesJsAdapter.js";
15
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";
16
14
  export { emailBuilderFeatures, isFeatureEnabled } from "./features/index.js";
17
15
  export { UiProvider, useUi, useT } from "./ui/context.jsx";
18
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";
19
19
  export { readDocument, createDocument, serializeDocument, EmailDocumentError } from "./core/document.js";
20
20
  export { EMAIL_BUILDER_VERSION, EMAIL_DOCUMENT_SCHEMA_VERSION, EMAIL_DEVICES } from "./core/constants.js";
21
21
  export { DEFAULT_VARIABLES, CUSTOM_GROUP, defineVariables, mergeVariables, keyFromLabel, validateCustomVariable, tokenOf } from "./features/variables/catalog.js";
22
22
  export { applyVariables, applyVariablesToText, usedVariables, unknownVariables } from "./features/variables/service.js";
23
23
  export { createStarterMjml, STARTER_MJML } from "./features/templates/starter.js";
24
- export { TEMPLATE_GALLERY, buildGallery } from "./features/templates/gallery.js";
24
+ export { TEMPLATE_GALLERY, buildGallery, withPlaceholderImages } from "./features/templates/gallery.js";
25
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";
26
30
  export { createTranslator, translatorFor, LOCALES } from "./i18n/index.js";
@@ -0,0 +1,5 @@
1
+ export { default as TemplateLibrary } from "./ui/library/TemplateLibrary.jsx";
2
+ export { EMAIL_BUILDER_VERSION } from "./core/constants.js";
3
+ export { createHttpStorage, createMemoryStorage, StorageError } from "./storage/client.js";
4
+ export { TEMPLATE_GALLERY, buildGallery, withPlaceholderImages } from "./features/templates/gallery.js";
5
+ 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,11 +101,21 @@ 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
  *
80
115
  * @typedef {{
116
+ * list?: () => Promise<StoredTemplate[]>,
81
117
  * load: (id: number|string) => Promise<StoredTemplate>,
118
+ * remove?: (id: number|string) => Promise<void>,
82
119
  * create: (payload: SavePayload & {name: string}) => Promise<{template: StoredTemplate, warnings: object[]}>,
83
120
  * duplicate: (id: number|string) => Promise<{template: StoredTemplate, warnings: object[]}>,
84
121
  * save: (id: number|string, payload: SavePayload) => Promise<{template: StoredTemplate, warnings: object[], version: {id: number|string, versionNumber: number}|null}>,
@@ -90,6 +127,12 @@ export function createMemoryStorage({ render, sendTest, assetsPath, systemVariab
90
127
  * listVariables?: () => Promise<EmailVariable[]>,
91
128
  * saveVariable?: (variable: Partial<EmailVariable>, options?: {create?: boolean}) => Promise<EmailVariable>,
92
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>,
93
136
  * }} TemplateStorage
94
137
  */
95
138
  export class StorageError extends Error {
@@ -147,6 +190,19 @@ export type RenderResult = {
147
190
  message: string;
148
191
  }[];
149
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
+ };
150
206
  export type TestResult = {
151
207
  email: string;
152
208
  ok: boolean;
@@ -159,7 +215,9 @@ export type SendTestResult = {
159
215
  results: TestResult[];
160
216
  };
161
217
  export type TemplateStorage = {
218
+ list?: () => Promise<StoredTemplate[]>;
162
219
  load: (id: number | string) => Promise<StoredTemplate>;
220
+ remove?: (id: number | string) => Promise<void>;
163
221
  create: (payload: SavePayload & {
164
222
  name: string;
165
223
  }) => Promise<{
@@ -197,4 +255,22 @@ export type TemplateStorage = {
197
255
  create?: boolean;
198
256
  }) => Promise<EmailVariable>;
199
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>;
200
276
  };
@@ -0,0 +1,42 @@
1
+ /**
2
+ * EMAIL STUDIO — the two screens wired together.
3
+ *
4
+ * TemplateLibrary ── Edit / New template / Start from this base ──▶ EmailBuilder
5
+ * ▲ │
6
+ * └──────────────────────────── Back ───────────────────────────┘
7
+ *
8
+ * Nothing here that a host could not write itself: both screens are exported
9
+ * on their own, and an application that only wants the editor mounts
10
+ * `EmailBuilder` alone (or the library alone, opening its own editor).
11
+ *
12
+ * Shared props go to both screens — except `templates` (the bases), which
13
+ * only the library shows: the editor gets `[]`, so no second gallery replaces
14
+ * the email being edited (`builderProps.templates` brings it back).
15
+ * `libraryProps` / `builderProps` reach only one. Coming back from the editor, the library selects the template that was
16
+ * just edited or created.
17
+ *
18
+ * @param {{
19
+ * storage?: import('../storage/client.js').TemplateStorage,
20
+ * locale?: string,
21
+ * messages?: Record<string, string>,
22
+ * onNotify?: (level: 'success'|'info'|'warning'|'error', message: string) => void,
23
+ * sender?: import('../features/templates/starter.js').StarterSender,
24
+ * templates?: import('../features/templates/gallery.js').GalleryTemplate[],
25
+ * onClose?: () => void,
26
+ * libraryProps?: object,
27
+ * builderProps?: object,
28
+ * className?: string,
29
+ * }} props `onClose`: shows the library's close button.
30
+ */
31
+ export default function EmailStudio({ storage: injectedStorage, locale, messages, onNotify, sender, templates, onClose, libraryProps, builderProps, className, }: {
32
+ storage?: import("../storage/client.js").TemplateStorage;
33
+ locale?: string;
34
+ messages?: Record<string, string>;
35
+ onNotify?: (level: "success" | "info" | "warning" | "error", message: string) => void;
36
+ sender?: import("../features/templates/starter.js").StarterSender;
37
+ templates?: import("../features/templates/gallery.js").GalleryTemplate[];
38
+ onClose?: () => void;
39
+ libraryProps?: object;
40
+ builderProps?: object;
41
+ className?: string;
42
+ }): import("react").JSX.Element;
@@ -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;
@@ -0,0 +1,41 @@
1
+ /**
2
+ * @param {{
3
+ * storage?: import('../../storage/client.js').TemplateStorage,
4
+ * locale?: string,
5
+ * messages?: Record<string, string>,
6
+ * onNotify?: (level: 'success'|'info'|'warning'|'error', message: string) => void,
7
+ * sender?: import('../../features/templates/starter.js').StarterSender,
8
+ * templates?: import('../../features/templates/gallery.js').GalleryTemplate[],
9
+ * title?: string,
10
+ * description?: string,
11
+ * selectedId?: string|number|null,
12
+ * favoritesKey?: string|null,
13
+ * onEdit?: (template: import('../../storage/client.js').StoredTemplate) => void,
14
+ * onCreate?: (start: null | {baseId: string, mjml: string, meta: {subject?: string, preheader?: string}}) => void,
15
+ * onClose?: () => void,
16
+ * className?: string,
17
+ * }} props
18
+ */
19
+ export default function TemplateLibrary({ locale, messages, onNotify, ...props }: {
20
+ storage?: import("../../storage/client.js").TemplateStorage;
21
+ locale?: string;
22
+ messages?: Record<string, string>;
23
+ onNotify?: (level: "success" | "info" | "warning" | "error", message: string) => void;
24
+ sender?: import("../../features/templates/starter.js").StarterSender;
25
+ templates?: import("../../features/templates/gallery.js").GalleryTemplate[];
26
+ title?: string;
27
+ description?: string;
28
+ selectedId?: string | number | null;
29
+ favoritesKey?: string | null;
30
+ onEdit?: (template: import("../../storage/client.js").StoredTemplate) => void;
31
+ onCreate?: (start: null | {
32
+ baseId: string;
33
+ mjml: string;
34
+ meta: {
35
+ subject?: string;
36
+ preheader?: string;
37
+ };
38
+ }) => void;
39
+ onClose?: () => void;
40
+ className?: string;
41
+ }): import("react").JSX.Element;