@julach-earzan/email-template-builder 0.1.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.
- package/LICENSE +21 -0
- package/README.md +87 -0
- package/dist/App.d.ts +2 -0
- package/dist/EmailBuilder.d.ts +35 -0
- package/dist/components/blocks/BlockPreview.d.ts +9 -0
- package/dist/components/blocks/index.d.ts +1 -0
- package/dist/components/editor/ComponentPalette.d.ts +7 -0
- package/dist/components/editor/EditorCanvas.d.ts +2 -0
- package/dist/components/editor/EditorDragOverlay.d.ts +13 -0
- package/dist/components/editor/EditorShell.d.ts +5 -0
- package/dist/components/editor/EditorToolbar.d.ts +2 -0
- package/dist/components/editor/IconUploadPanel.d.ts +10 -0
- package/dist/components/editor/ImageUploadPanel.d.ts +7 -0
- package/dist/components/editor/PropertiesPanel.d.ts +5 -0
- package/dist/components/editor/SortableBlockRow.d.ts +12 -0
- package/dist/components/editor/emailBuilderCollision.d.ts +6 -0
- package/dist/components/editor/paletteIcons.d.ts +5 -0
- package/dist/components/editor/useEditorDragEnd.d.ts +5 -0
- package/dist/components/layout/ColumnCell.d.ts +16 -0
- package/dist/components/layout/SortableSectionRow.d.ts +13 -0
- package/dist/context/EmailBuilderApiProvider.d.ts +7 -0
- package/dist/context/emailBuilderApiContext.d.ts +23 -0
- package/dist/context/useEmailBuilderApi.d.ts +1 -0
- package/dist/core/blockFactory.d.ts +3 -0
- package/dist/core/constants.d.ts +7 -0
- package/dist/core/exampleTemplate.d.ts +3 -0
- package/dist/core/index.d.ts +4 -0
- package/dist/email-template-builder.css +3 -0
- package/dist/favicon.svg +1 -0
- package/dist/icons.svg +24 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +8067 -0
- package/dist/main.d.ts +1 -0
- package/dist/store/emailStore.d.ts +39 -0
- package/dist/types/email.d.ts +92 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/utils/emailBuilderApi.d.ts +30 -0
- package/dist/utils/generateEmailHTML.d.ts +13 -0
- package/dist/utils/id.d.ts +2 -0
- package/dist/utils/index.d.ts +2 -0
- package/dist/utils/normalizeEmailTemplate.d.ts +8 -0
- package/dist/utils/styles.d.ts +6 -0
- package/dist/utils/templateQueries.d.ts +19 -0
- package/dist/utils/textAlign.d.ts +5 -0
- package/package.json +82 -0
package/dist/main.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { BlockType, EditorSelection, EmailComponent, EmailStyles, EmailTemplate } from '../types';
|
|
2
|
+
export type EmailStore = {
|
|
3
|
+
template: EmailTemplate;
|
|
4
|
+
selected: EditorSelection | null;
|
|
5
|
+
past: EmailTemplate[];
|
|
6
|
+
future: EmailTemplate[];
|
|
7
|
+
setTemplate: (t: EmailTemplate) => void;
|
|
8
|
+
updateDocument: (patch: Partial<Pick<EmailTemplate, 'width' | 'bodyStyles' | 'meta' | 'documentName'>>) => void;
|
|
9
|
+
previewDevice: 'desktop' | 'mobile';
|
|
10
|
+
setPreviewDevice: (d: 'desktop' | 'mobile') => void;
|
|
11
|
+
canvasView: 'editor' | 'preview';
|
|
12
|
+
setCanvasView: (v: 'editor' | 'preview') => void;
|
|
13
|
+
select: (sel: EditorSelection | null) => void;
|
|
14
|
+
addSection: (atIndex?: number) => void;
|
|
15
|
+
removeSection: (sectionId: string) => void;
|
|
16
|
+
reorderSections: (activeIndex: number, overIndex: number) => void;
|
|
17
|
+
setSectionColumnCount: (sectionId: string, count: number) => void;
|
|
18
|
+
updateSectionStyles: (sectionId: string, styles: Partial<EmailStyles>) => void;
|
|
19
|
+
updateColumnStyles: (sectionId: string, columnId: string, styles: Partial<EmailStyles>) => void;
|
|
20
|
+
insertBlockAtColumn: (columnId: string, index: number, type: BlockType) => void;
|
|
21
|
+
moveBlock: (blockId: string, toColumnId: string, toIndex: number) => void;
|
|
22
|
+
reorderInColumn: (columnId: string, fromIndex: number, toIndex: number) => void;
|
|
23
|
+
duplicateComponent: (componentId: string) => void;
|
|
24
|
+
removeComponent: (componentId: string) => void;
|
|
25
|
+
updateComponent: (componentId: string, patch: Partial<{
|
|
26
|
+
content: EmailComponent['content'];
|
|
27
|
+
styles: Partial<EmailStyles>;
|
|
28
|
+
settings: EmailComponent['settings'];
|
|
29
|
+
}>) => void;
|
|
30
|
+
deleteComponent: (componentId: string) => void;
|
|
31
|
+
updateComponentStyles: (componentId: string, styles: Partial<EmailStyles>) => void;
|
|
32
|
+
updateComponentContent: (componentId: string, content: EmailComponent['content']) => void;
|
|
33
|
+
selectComponent: (componentId: string) => void;
|
|
34
|
+
undo: () => void;
|
|
35
|
+
redo: () => void;
|
|
36
|
+
/** Replace history tip (e.g. after load) */
|
|
37
|
+
resetHistory: () => void;
|
|
38
|
+
};
|
|
39
|
+
export declare const useEmailStore: import('zustand').UseBoundStore<import('zustand').StoreApi<EmailStore>>;
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Email template JSON model: sections → columns → components.
|
|
3
|
+
* Styles use camelCase keys; they are inlined in export (converted for HTML attributes).
|
|
4
|
+
*/
|
|
5
|
+
export type BlockType = 'text' | 'image' | 'button' | 'divider' | 'spacer';
|
|
6
|
+
/** Inline-style map compatible with email clients (string values). */
|
|
7
|
+
export type EmailStyles = Record<string, string>;
|
|
8
|
+
/** Semantic tag for export (paragraph vs headings). */
|
|
9
|
+
export type TextVariant = 'p' | 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6';
|
|
10
|
+
/** Plain text plus whole-block formatting (email-safe tags in HTML export). */
|
|
11
|
+
export type TextContent = {
|
|
12
|
+
text: string;
|
|
13
|
+
variant?: TextVariant;
|
|
14
|
+
listType?: 'none' | 'ul' | 'ol';
|
|
15
|
+
bold?: boolean;
|
|
16
|
+
italic?: boolean;
|
|
17
|
+
underline?: boolean;
|
|
18
|
+
strikethrough?: boolean;
|
|
19
|
+
subscript?: boolean;
|
|
20
|
+
superscript?: boolean;
|
|
21
|
+
};
|
|
22
|
+
export type ImageContent = {
|
|
23
|
+
src: string;
|
|
24
|
+
alt: string;
|
|
25
|
+
};
|
|
26
|
+
export type ButtonContent = {
|
|
27
|
+
label: string;
|
|
28
|
+
href: string;
|
|
29
|
+
/**
|
|
30
|
+
* Optional icon shown next to the label (preview + export).
|
|
31
|
+
* Use empty `src` to represent "icon disabled".
|
|
32
|
+
*/
|
|
33
|
+
icon?: {
|
|
34
|
+
src: string;
|
|
35
|
+
alt: string;
|
|
36
|
+
};
|
|
37
|
+
};
|
|
38
|
+
export type DividerContent = Record<string, never>;
|
|
39
|
+
export type SpacerContent = {
|
|
40
|
+
height: string;
|
|
41
|
+
};
|
|
42
|
+
export type BlockContent = TextContent | ImageContent | ButtonContent | DividerContent | SpacerContent;
|
|
43
|
+
export type EmailComponent = {
|
|
44
|
+
id: string;
|
|
45
|
+
type: BlockType;
|
|
46
|
+
content: BlockContent;
|
|
47
|
+
styles: EmailStyles;
|
|
48
|
+
settings?: {
|
|
49
|
+
/** Hide component in editor + export. */
|
|
50
|
+
hidden?: boolean;
|
|
51
|
+
/** Toggle device-specific visibility in editor + (desktop export only). */
|
|
52
|
+
deviceVisibility?: {
|
|
53
|
+
desktop: boolean;
|
|
54
|
+
mobile: boolean;
|
|
55
|
+
};
|
|
56
|
+
};
|
|
57
|
+
};
|
|
58
|
+
export type EmailColumn = {
|
|
59
|
+
id: string;
|
|
60
|
+
components: EmailComponent[];
|
|
61
|
+
/** Optional width hint for export (e.g. "50%"); defaults by column count. */
|
|
62
|
+
styles?: EmailStyles;
|
|
63
|
+
};
|
|
64
|
+
export type EmailSection = {
|
|
65
|
+
id: string;
|
|
66
|
+
/** 1–3 columns per product requirement */
|
|
67
|
+
columns: EmailColumn[];
|
|
68
|
+
styles: EmailStyles;
|
|
69
|
+
};
|
|
70
|
+
export type EmailTemplate = {
|
|
71
|
+
id: string;
|
|
72
|
+
/** Builder document title (e.g. “New Message”). */
|
|
73
|
+
documentName?: string;
|
|
74
|
+
meta?: {
|
|
75
|
+
subject?: string;
|
|
76
|
+
preheader?: string;
|
|
77
|
+
};
|
|
78
|
+
/** Max width of inner email body (e.g. 600px) */
|
|
79
|
+
width: string;
|
|
80
|
+
bodyStyles: EmailStyles;
|
|
81
|
+
sections: EmailSection[];
|
|
82
|
+
};
|
|
83
|
+
export type SelectionKind = 'section' | 'column' | 'component';
|
|
84
|
+
export type EditorSelection = {
|
|
85
|
+
kind: SelectionKind;
|
|
86
|
+
/** section id, column id, or component id */
|
|
87
|
+
id: string;
|
|
88
|
+
/** When kind is column or component, parent section id */
|
|
89
|
+
sectionId?: string;
|
|
90
|
+
/** When kind is component, parent column id */
|
|
91
|
+
columnId?: string;
|
|
92
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export type { BlockContent, BlockType, ButtonContent, DividerContent, EditorSelection, EmailColumn, EmailComponent, EmailSection, EmailStyles, EmailTemplate, ImageContent, SelectionKind, SpacerContent, TextContent, TextVariant, } from './email';
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { EmailTemplate } from '../types';
|
|
2
|
+
/** HTTP verb; lower or upper case accepted. */
|
|
3
|
+
export type ApiEndpoint = {
|
|
4
|
+
method: string;
|
|
5
|
+
url: string;
|
|
6
|
+
headers?: Record<string, string>;
|
|
7
|
+
};
|
|
8
|
+
export declare function normalizeHttpMethod(method: string): string;
|
|
9
|
+
export declare function parseImageUrlFromResponse(data: unknown, custom?: (json: unknown) => string | undefined): string | undefined;
|
|
10
|
+
export declare function resolveUrlMaybeRelative(href: string, baseUrl: string): string;
|
|
11
|
+
export declare function uploadFileToEndpoint(endpoint: ApiEndpoint, file: File, options: {
|
|
12
|
+
fieldName: string;
|
|
13
|
+
credentials?: RequestCredentials;
|
|
14
|
+
parseUploadResponse?: (json: unknown) => string | undefined;
|
|
15
|
+
}): Promise<string>;
|
|
16
|
+
export declare function postEmailExport(endpoint: ApiEndpoint, payload: {
|
|
17
|
+
html: string;
|
|
18
|
+
template: EmailTemplate;
|
|
19
|
+
}, options: {
|
|
20
|
+
credentials?: RequestCredentials;
|
|
21
|
+
buildExportBody?: (p: {
|
|
22
|
+
html: string;
|
|
23
|
+
template: EmailTemplate;
|
|
24
|
+
}) => BodyInit;
|
|
25
|
+
buildExportHeaders?: (p: {
|
|
26
|
+
html: string;
|
|
27
|
+
template: EmailTemplate;
|
|
28
|
+
}) => Record<string, string>;
|
|
29
|
+
}): Promise<Response>;
|
|
30
|
+
export declare function fetchTemplateFromEndpoint(endpoint: ApiEndpoint, credentials?: RequestCredentials): Promise<EmailTemplate>;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { EmailTemplate } from '../types';
|
|
2
|
+
/**
|
|
3
|
+
* Convert a typed template object to a full HTML document (tables + inline CSS).
|
|
4
|
+
*/
|
|
5
|
+
export declare function generateEmailHTML(templateJson: EmailTemplate): string;
|
|
6
|
+
/**
|
|
7
|
+
* Parse JSON string → normalize → HTML. Throws `SyntaxError` if JSON is invalid.
|
|
8
|
+
*/
|
|
9
|
+
export declare function generateEmailHTMLFromJson(json: string): string;
|
|
10
|
+
/**
|
|
11
|
+
* Accept unknown parsed JSON (e.g. `fetch().json()`) and return HTML safely.
|
|
12
|
+
*/
|
|
13
|
+
export declare function generateEmailHTMLFromUnknown(data: unknown): string;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { EmailStyles, EmailTemplate } from '../types';
|
|
2
|
+
export declare function normalizeStyles(v: unknown): EmailStyles;
|
|
3
|
+
/** Default empty template when input is not an object. */
|
|
4
|
+
export declare function emptyEmailTemplate(): EmailTemplate;
|
|
5
|
+
/**
|
|
6
|
+
* Normalize arbitrary JSON (e.g. from API or file) into EmailTemplate.
|
|
7
|
+
*/
|
|
8
|
+
export declare function normalizeEmailTemplate(input: unknown): EmailTemplate;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { CSSProperties } from 'react';
|
|
2
|
+
import { EmailStyles } from '../types';
|
|
3
|
+
/** Convert stored style map to React inline `style` object. */
|
|
4
|
+
export declare function stylesToReact(styles: EmailStyles | undefined): CSSProperties;
|
|
5
|
+
/** Serialize to a single `style="..."` attribute value for email HTML. */
|
|
6
|
+
export declare function stylesToInlineAttr(styles: EmailStyles): string;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { EmailColumn, EmailComponent, EmailSection, EmailTemplate } from '../types';
|
|
2
|
+
export type BlockLocation = {
|
|
3
|
+
sectionId: string;
|
|
4
|
+
columnId: string;
|
|
5
|
+
index: number;
|
|
6
|
+
};
|
|
7
|
+
export declare function getSectionById(template: EmailTemplate, sectionId: string): EmailSection | null;
|
|
8
|
+
export declare function findBlockLocation(template: EmailTemplate, blockId: string): BlockLocation | null;
|
|
9
|
+
export declare function getColumnById(template: EmailTemplate, columnId: string): EmailColumn | null;
|
|
10
|
+
export declare function findComponentById(template: EmailTemplate, componentId: string): {
|
|
11
|
+
component: EmailComponent;
|
|
12
|
+
sectionId: string;
|
|
13
|
+
columnId: string;
|
|
14
|
+
} | null;
|
|
15
|
+
/** Palette drop: insert before a block, or append when `over` is the column droppable. */
|
|
16
|
+
export declare function resolveColumnInsertIndex(template: EmailTemplate, overId: string): {
|
|
17
|
+
columnId: string;
|
|
18
|
+
index: number;
|
|
19
|
+
} | null;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export type EmailAlign = 'left' | 'center' | 'right' | 'justify';
|
|
2
|
+
/** Map style `textAlign` to CSS; `td align` is only left/center/right (justify uses left + text-align). */
|
|
3
|
+
export declare function emailTextAlign(textAlign: string | undefined): EmailAlign;
|
|
4
|
+
/** HTML `align` attribute on <td> (no `justify` in classic email). */
|
|
5
|
+
export declare function emailTdAlign(align: EmailAlign): 'left' | 'center' | 'right';
|
package/package.json
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@julach-earzan/email-template-builder",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "React email template editor with drag-and-drop blocks, preview, HTML export, and optional upload/export API hooks.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"author": "julach-earzan",
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"keywords": [
|
|
9
|
+
"react",
|
|
10
|
+
"email",
|
|
11
|
+
"template",
|
|
12
|
+
"editor",
|
|
13
|
+
"builder",
|
|
14
|
+
"html",
|
|
15
|
+
"newsletter",
|
|
16
|
+
"drag-and-drop"
|
|
17
|
+
],
|
|
18
|
+
"repository": {
|
|
19
|
+
"type": "git",
|
|
20
|
+
"url": "https://github.com/julach-earzan/email-template-builder.git"
|
|
21
|
+
},
|
|
22
|
+
"publishConfig": {
|
|
23
|
+
"access": "public"
|
|
24
|
+
},
|
|
25
|
+
"main": "./dist/index.js",
|
|
26
|
+
"module": "./dist/index.js",
|
|
27
|
+
"types": "./dist/index.d.ts",
|
|
28
|
+
"exports": {
|
|
29
|
+
".": {
|
|
30
|
+
"types": "./dist/index.d.ts",
|
|
31
|
+
"import": "./dist/index.js"
|
|
32
|
+
},
|
|
33
|
+
"./style.css": "./dist/email-template-builder.css"
|
|
34
|
+
},
|
|
35
|
+
"sideEffects": [
|
|
36
|
+
"**/*.css"
|
|
37
|
+
],
|
|
38
|
+
"files": [
|
|
39
|
+
"dist",
|
|
40
|
+
"README.md",
|
|
41
|
+
"LICENSE"
|
|
42
|
+
],
|
|
43
|
+
"peerDependencies": {
|
|
44
|
+
"react": ">=18.0.0",
|
|
45
|
+
"react-dom": ">=18.0.0"
|
|
46
|
+
},
|
|
47
|
+
"scripts": {
|
|
48
|
+
"dev": "vite",
|
|
49
|
+
"build": "tsc -b && vite build",
|
|
50
|
+
"build:lib": "vite build --config vite.lib.config.ts",
|
|
51
|
+
"lint": "eslint .",
|
|
52
|
+
"preview": "vite preview",
|
|
53
|
+
"prepublishOnly": "npm run build:lib"
|
|
54
|
+
},
|
|
55
|
+
"dependencies": {
|
|
56
|
+
"@dnd-kit/core": "^6.3.1",
|
|
57
|
+
"@dnd-kit/sortable": "^10.0.0",
|
|
58
|
+
"@dnd-kit/utilities": "^3.2.2",
|
|
59
|
+
"dotenv": "^17.4.0",
|
|
60
|
+
"react": "^19.2.4",
|
|
61
|
+
"react-dom": "^19.2.4",
|
|
62
|
+
"react-icons": "^5.6.0",
|
|
63
|
+
"zustand": "^5.0.12"
|
|
64
|
+
},
|
|
65
|
+
"devDependencies": {
|
|
66
|
+
"@eslint/js": "^9.39.4",
|
|
67
|
+
"@tailwindcss/vite": "^4.2.2",
|
|
68
|
+
"@types/node": "^24.12.0",
|
|
69
|
+
"@types/react": "^19.2.14",
|
|
70
|
+
"@types/react-dom": "^19.2.3",
|
|
71
|
+
"@vitejs/plugin-react": "^6.0.1",
|
|
72
|
+
"eslint": "^9.39.4",
|
|
73
|
+
"eslint-plugin-react-hooks": "^7.0.1",
|
|
74
|
+
"eslint-plugin-react-refresh": "^0.5.2",
|
|
75
|
+
"globals": "^17.4.0",
|
|
76
|
+
"tailwindcss": "^4.2.2",
|
|
77
|
+
"typescript": "~5.9.3",
|
|
78
|
+
"typescript-eslint": "^8.57.0",
|
|
79
|
+
"vite": "^8.0.1",
|
|
80
|
+
"vite-plugin-dts": "^4.5.4"
|
|
81
|
+
}
|
|
82
|
+
}
|