email-builder-pro 1.0.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.
@@ -0,0 +1,104 @@
1
+ 'use client';
2
+
3
+ import { useEmailBuilder } from '@/lib/store';
4
+ import { X, Trash2, FolderOpen } from 'lucide-react';
5
+ import { EmailTemplate } from '@/types/email';
6
+
7
+ interface TemplateManagerProps {
8
+ onClose: () => void;
9
+ }
10
+
11
+ export default function TemplateManager({ onClose }: TemplateManagerProps) {
12
+ const { templates, loadTemplate, deleteTemplate, clearComponents } =
13
+ useEmailBuilder();
14
+
15
+ const handleLoadTemplate = (template: EmailTemplate) => {
16
+ loadTemplate(template);
17
+ onClose();
18
+ };
19
+
20
+ const handleDeleteTemplate = (id: string, e: React.MouseEvent) => {
21
+ e.stopPropagation();
22
+ if (confirm('Are you sure you want to delete this template?')) {
23
+ deleteTemplate(id);
24
+ }
25
+ };
26
+
27
+ return (
28
+ <div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
29
+ <div className="bg-white rounded-lg w-full max-w-2xl max-h-[80vh] flex flex-col">
30
+ <div className="p-6 border-b border-gray-200 flex items-center justify-between">
31
+ <h2 className="text-xl font-semibold text-gray-800">Templates</h2>
32
+ <button
33
+ onClick={onClose}
34
+ className="p-2 hover:bg-gray-100 rounded-md transition-colors"
35
+ >
36
+ <X size={20} />
37
+ </button>
38
+ </div>
39
+
40
+ <div className="flex-1 overflow-y-auto p-6">
41
+ {templates.length === 0 ? (
42
+ <div className="text-center py-12">
43
+ <FolderOpen size={48} className="mx-auto text-gray-300 mb-4" />
44
+ <p className="text-gray-400 mb-2">No templates saved yet</p>
45
+ <p className="text-sm text-gray-300">
46
+ Create and save templates to see them here
47
+ </p>
48
+ </div>
49
+ ) : (
50
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
51
+ {templates.map((template) => (
52
+ <div
53
+ key={template.id}
54
+ className="border border-gray-200 rounded-lg p-4 hover:border-primary-300 hover:shadow-md transition-all cursor-pointer group"
55
+ onClick={() => handleLoadTemplate(template)}
56
+ >
57
+ <div className="flex items-start justify-between mb-2">
58
+ <h3 className="font-medium text-gray-800">
59
+ {template.name}
60
+ </h3>
61
+ <button
62
+ onClick={(e) => handleDeleteTemplate(template.id, e)}
63
+ className="opacity-0 group-hover:opacity-100 p-1 text-red-600 hover:bg-red-50 rounded transition-all"
64
+ >
65
+ <Trash2 size={16} />
66
+ </button>
67
+ </div>
68
+ <p className="text-xs text-gray-500 mb-2">
69
+ {template.components.length} component
70
+ {template.components.length !== 1 ? 's' : ''}
71
+ </p>
72
+ <p className="text-xs text-gray-400">
73
+ Created:{' '}
74
+ {new Date(template.createdAt).toLocaleDateString()}
75
+ </p>
76
+ </div>
77
+ ))}
78
+ </div>
79
+ )}
80
+ </div>
81
+
82
+ <div className="p-6 border-t border-gray-200 flex justify-end gap-2">
83
+ <button
84
+ onClick={() => {
85
+ clearComponents();
86
+ onClose();
87
+ }}
88
+ className="px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-100 rounded-md"
89
+ >
90
+ New Template
91
+ </button>
92
+ <button
93
+ onClick={onClose}
94
+ className="px-4 py-2 text-sm font-medium bg-primary-500 text-white rounded-md hover:bg-primary-600"
95
+ >
96
+ Close
97
+ </button>
98
+ </div>
99
+ </div>
100
+ </div>
101
+ );
102
+ }
103
+
104
+
@@ -0,0 +1,242 @@
1
+ 'use client';
2
+
3
+ import { Eye, Save, Download, FileText, FolderOpen } from 'lucide-react';
4
+ import { useEmailBuilder } from '@/lib/store';
5
+ import { useState } from 'react';
6
+
7
+ interface ToolbarProps {
8
+ onPreview: () => void;
9
+ onTemplates: () => void;
10
+ showPreview: boolean;
11
+ }
12
+
13
+ export default function Toolbar({ onPreview, onTemplates, showPreview }: ToolbarProps) {
14
+ const { components, saveTemplate, clearComponents } = useEmailBuilder();
15
+ const [templateName, setTemplateName] = useState('');
16
+ const [showSaveDialog, setShowSaveDialog] = useState(false);
17
+
18
+ const handleSave = () => {
19
+ if (templateName.trim()) {
20
+ saveTemplate(templateName);
21
+ setTemplateName('');
22
+ setShowSaveDialog(false);
23
+ }
24
+ };
25
+
26
+ const handleExportHTML = () => {
27
+ const html = generateHTML(components);
28
+ const blob = new Blob([html], { type: 'text/html' });
29
+ const url = URL.createObjectURL(blob);
30
+ const a = document.createElement('a');
31
+ a.href = url;
32
+ a.download = 'email-template.html';
33
+ a.click();
34
+ URL.revokeObjectURL(url);
35
+ };
36
+
37
+ const handleExportReact = () => {
38
+ const reactCode = generateReactComponent(components);
39
+ const blob = new Blob([reactCode], { type: 'text/tsx' });
40
+ const url = URL.createObjectURL(blob);
41
+ const a = document.createElement('a');
42
+ a.href = url;
43
+ a.download = 'email-template.tsx';
44
+ a.click();
45
+ URL.revokeObjectURL(url);
46
+ };
47
+
48
+ return (
49
+ <div className="h-14 bg-white border-b border-gray-200 flex items-center justify-between px-4 shadow-sm">
50
+ <div className="flex items-center gap-2">
51
+ <h1 className="text-xl font-bold text-gray-800">Email Builder</h1>
52
+ </div>
53
+
54
+ <div className="flex items-center gap-2">
55
+ <button
56
+ onClick={onTemplates}
57
+ className="px-3 py-1.5 text-sm font-medium text-gray-700 hover:bg-gray-100 rounded-md flex items-center gap-2"
58
+ >
59
+ <FolderOpen size={16} />
60
+ Templates
61
+ </button>
62
+
63
+ <button
64
+ onClick={() => setShowSaveDialog(true)}
65
+ className="px-3 py-1.5 text-sm font-medium text-gray-700 hover:bg-gray-100 rounded-md flex items-center gap-2"
66
+ >
67
+ <Save size={16} />
68
+ Save
69
+ </button>
70
+
71
+ <div className="w-px h-6 bg-gray-300 mx-1" />
72
+
73
+ <button
74
+ onClick={handleExportHTML}
75
+ disabled={components.length === 0}
76
+ className="px-3 py-1.5 text-sm font-medium text-gray-700 hover:bg-gray-100 rounded-md flex items-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed"
77
+ >
78
+ <Download size={16} />
79
+ Export HTML
80
+ </button>
81
+
82
+ <button
83
+ onClick={handleExportReact}
84
+ disabled={components.length === 0}
85
+ className="px-3 py-1.5 text-sm font-medium text-gray-700 hover:bg-gray-100 rounded-md flex items-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed"
86
+ >
87
+ <FileText size={16} />
88
+ Export React
89
+ </button>
90
+
91
+ <div className="w-px h-6 bg-gray-300 mx-1" />
92
+
93
+ <button
94
+ onClick={onPreview}
95
+ className={`px-3 py-1.5 text-sm font-medium rounded-md flex items-center gap-2 ${
96
+ showPreview
97
+ ? 'bg-primary-500 text-white'
98
+ : 'text-gray-700 hover:bg-gray-100'
99
+ }`}
100
+ >
101
+ <Eye size={16} />
102
+ Preview
103
+ </button>
104
+ </div>
105
+
106
+ {showSaveDialog && (
107
+ <div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
108
+ <div className="bg-white rounded-lg p-6 w-96">
109
+ <h2 className="text-lg font-semibold mb-4">Save Template</h2>
110
+ <input
111
+ type="text"
112
+ value={templateName}
113
+ onChange={(e) => setTemplateName(e.target.value)}
114
+ placeholder="Template name"
115
+ className="w-full px-3 py-2 border border-gray-300 rounded-md mb-4"
116
+ autoFocus
117
+ onKeyDown={(e) => {
118
+ if (e.key === 'Enter') handleSave();
119
+ if (e.key === 'Escape') setShowSaveDialog(false);
120
+ }}
121
+ />
122
+ <div className="flex justify-end gap-2">
123
+ <button
124
+ onClick={() => setShowSaveDialog(false)}
125
+ className="px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-100 rounded-md"
126
+ >
127
+ Cancel
128
+ </button>
129
+ <button
130
+ onClick={handleSave}
131
+ disabled={!templateName.trim()}
132
+ className="px-4 py-2 text-sm font-medium bg-primary-500 text-white rounded-md hover:bg-primary-600 disabled:opacity-50 disabled:cursor-not-allowed"
133
+ >
134
+ Save
135
+ </button>
136
+ </div>
137
+ </div>
138
+ </div>
139
+ )}
140
+ </div>
141
+ );
142
+ }
143
+
144
+ function generateHTML(components: any[]): string {
145
+ // This is a simplified version - in production, you'd want a more robust HTML generator
146
+ return `<!DOCTYPE html>
147
+ <html>
148
+ <head>
149
+ <meta charset="utf-8">
150
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
151
+ <title>Email Template</title>
152
+ </head>
153
+ <body>
154
+ ${renderComponentsToHTML(components)}
155
+ </body>
156
+ </html>`;
157
+ }
158
+
159
+ function renderComponentsToHTML(components: any[]): string {
160
+ return components.map(comp => {
161
+ switch (comp.type) {
162
+ case 'container':
163
+ return `<div style="${getStyles(comp.props)}">${comp.children ? renderComponentsToHTML(comp.children) : ''}</div>`;
164
+ case 'text':
165
+ return `<p style="${getStyles(comp.props)}">${comp.props.text || ''}</p>`;
166
+ case 'heading':
167
+ return `<h${comp.props.level || 1} style="${getStyles(comp.props)}">${comp.props.text || ''}</h${comp.props.level || 1}>`;
168
+ case 'button':
169
+ return `<a href="${comp.props.href || '#'}" style="${getStyles(comp.props)}">${comp.props.text || 'Button'}</a>`;
170
+ case 'image':
171
+ return `<img src="${comp.props.src || ''}" alt="${comp.props.alt || ''}" style="${getStyles(comp.props)}" />`;
172
+ case 'divider':
173
+ return `<hr style="${getStyles(comp.props)}" />`;
174
+ default:
175
+ return '';
176
+ }
177
+ }).join('');
178
+ }
179
+
180
+ function getStyles(props: any): string {
181
+ const styles: string[] = [];
182
+ if (props.backgroundColor) styles.push(`background-color: ${props.backgroundColor}`);
183
+ if (props.color) styles.push(`color: ${props.color}`);
184
+ if (props.padding) styles.push(`padding: ${props.padding}px`);
185
+ if (props.margin) styles.push(`margin: ${props.margin}px`);
186
+ if (props.fontSize) styles.push(`font-size: ${props.fontSize}px`);
187
+ if (props.textAlign) styles.push(`text-align: ${props.textAlign}`);
188
+ return styles.join('; ');
189
+ }
190
+
191
+ function generateReactComponent(components: any[]): string {
192
+ return `import {
193
+ Body,
194
+ Container,
195
+ Head,
196
+ Html,
197
+ Preview,
198
+ Tailwind,
199
+ } from '@react-email/components';
200
+
201
+ export default function EmailTemplate() {
202
+ return (
203
+ <Html>
204
+ <Head />
205
+ <Preview>Email Preview</Preview>
206
+ <Tailwind>
207
+ <Body className="mx-auto my-auto bg-white px-2 font-sans">
208
+ <Container className="mx-auto my-[40px] max-w-[600px]">
209
+ ${renderComponentsToReact(components)}
210
+ </Container>
211
+ </Body>
212
+ </Tailwind>
213
+ </Html>
214
+ );
215
+ }
216
+ `;
217
+ }
218
+
219
+ function renderComponentsToReact(components: any[]): string {
220
+ return components.map(comp => {
221
+ switch (comp.type) {
222
+ case 'text':
223
+ return `<Text className="${getClassName(comp.props)}">${comp.props.text || ''}</Text>`;
224
+ case 'heading':
225
+ return `<Heading className="${getClassName(comp.props)}">${comp.props.text || ''}</Heading>`;
226
+ case 'button':
227
+ return `<Button href="${comp.props.href || '#'}" className="${getClassName(comp.props)}">${comp.props.text || 'Button'}</Button>`;
228
+ case 'image':
229
+ return `<Img src="${comp.props.src || ''}" alt="${comp.props.alt || ''}" className="${getClassName(comp.props)}" />`;
230
+ default:
231
+ return '';
232
+ }
233
+ }).join('\n ');
234
+ }
235
+
236
+ function getClassName(props: any): string {
237
+ const classes: string[] = [];
238
+ if (props.textAlign) classes.push(`text-${props.textAlign}`);
239
+ if (props.fontSize) classes.push(`text-[${props.fontSize}px]`);
240
+ return classes.join(' ');
241
+ }
242
+
package/lib/store.ts ADDED
@@ -0,0 +1,198 @@
1
+ import { create } from 'zustand';
2
+ import { EmailComponent, EmailTemplate } from '@/types/email';
3
+
4
+ interface EmailBuilderState {
5
+ components: EmailComponent[];
6
+ selectedComponent: string | null;
7
+ templates: EmailTemplate[];
8
+ currentTemplate: EmailTemplate | null;
9
+
10
+ // Actions
11
+ addComponent: (component: EmailComponent, index?: number, parentId?: string | null) => void;
12
+ removeComponent: (id: string) => void;
13
+ updateComponent: (id: string, props: Partial<EmailComponent>) => void;
14
+ selectComponent: (id: string | null) => void;
15
+ moveComponent: (fromIndex: number, toIndex: number) => void;
16
+ clearComponents: () => void;
17
+ saveTemplate: (name: string) => void;
18
+ loadTemplate: (template: EmailTemplate) => void;
19
+ deleteTemplate: (id: string) => void;
20
+ }
21
+
22
+ const defaultTemplate: EmailTemplate = {
23
+ id: 'default',
24
+ name: 'New Template',
25
+ components: [],
26
+ createdAt: new Date().toISOString(),
27
+ };
28
+
29
+ // Helper function to recursively find and update components
30
+ const findAndUpdateComponent = (
31
+ components: EmailComponent[],
32
+ id: string,
33
+ updater: (comp: EmailComponent) => EmailComponent
34
+ ): EmailComponent[] => {
35
+ return components.map((comp) => {
36
+ if (comp.id === id) {
37
+ return updater(comp);
38
+ }
39
+ if (comp.children) {
40
+ return {
41
+ ...comp,
42
+ children: findAndUpdateComponent(comp.children, id, updater),
43
+ };
44
+ }
45
+ return comp;
46
+ });
47
+ };
48
+
49
+ // Helper function to recursively find and remove components
50
+ const findAndRemoveComponent = (
51
+ components: EmailComponent[],
52
+ id: string
53
+ ): EmailComponent[] => {
54
+ return components
55
+ .filter((comp) => comp.id !== id)
56
+ .map((comp) => {
57
+ if (comp.children) {
58
+ return {
59
+ ...comp,
60
+ children: findAndRemoveComponent(comp.children, id),
61
+ };
62
+ }
63
+ return comp;
64
+ });
65
+ };
66
+
67
+ // Helper function to find a component by ID
68
+ const findComponent = (
69
+ components: EmailComponent[],
70
+ id: string
71
+ ): EmailComponent | null => {
72
+ for (const comp of components) {
73
+ if (comp.id === id) {
74
+ return comp;
75
+ }
76
+ if (comp.children) {
77
+ const found = findComponent(comp.children, id);
78
+ if (found) return found;
79
+ }
80
+ }
81
+ return null;
82
+ };
83
+
84
+ export const useEmailBuilder = create<EmailBuilderState>((set) => ({
85
+ components: [],
86
+ selectedComponent: null,
87
+ templates: [],
88
+ currentTemplate: null,
89
+
90
+ addComponent: (component, index, parentId) =>
91
+ set((state) => {
92
+ // Check if component with this ID already exists
93
+ const componentExists = (comps: EmailComponent[], id: string): boolean => {
94
+ for (const comp of comps) {
95
+ if (comp.id === id) return true;
96
+ if (comp.children && componentExists(comp.children, id)) return true;
97
+ }
98
+ return false;
99
+ };
100
+
101
+ if (componentExists(state.components, component.id)) {
102
+ // Component already exists, don't add again
103
+ return state;
104
+ }
105
+
106
+ if (parentId) {
107
+ // Add to nested component
108
+ const updatedComponents = findAndUpdateComponent(
109
+ state.components,
110
+ parentId,
111
+ (parent) => {
112
+ const children = parent.children || [];
113
+ // Check for duplicate in children
114
+ if (children.some((child) => child.id === component.id)) {
115
+ return parent;
116
+ }
117
+ const newChildren = [...children];
118
+ if (index !== undefined) {
119
+ newChildren.splice(index, 0, component);
120
+ } else {
121
+ newChildren.push(component);
122
+ }
123
+ return { ...parent, children: newChildren };
124
+ }
125
+ );
126
+ return { components: updatedComponents };
127
+ } else {
128
+ // Add to root level
129
+ // Check for duplicate at root level
130
+ if (state.components.some((c) => c.id === component.id)) {
131
+ return state;
132
+ }
133
+ const newComponents = [...state.components];
134
+ if (index !== undefined) {
135
+ newComponents.splice(index, 0, component);
136
+ } else {
137
+ newComponents.push(component);
138
+ }
139
+ return { components: newComponents };
140
+ }
141
+ }),
142
+
143
+ removeComponent: (id) =>
144
+ set((state) => ({
145
+ components: findAndRemoveComponent(state.components, id),
146
+ selectedComponent: state.selectedComponent === id ? null : state.selectedComponent,
147
+ })),
148
+
149
+
150
+ updateComponent: (id, props) =>
151
+ set((state) => ({
152
+ components: findAndUpdateComponent(state.components, id, (comp) => ({
153
+ ...comp,
154
+ ...props,
155
+ })),
156
+ })),
157
+
158
+ selectComponent: (id) => set({ selectedComponent: id }),
159
+
160
+ moveComponent: (fromIndex, toIndex) =>
161
+ set((state) => {
162
+ const newComponents = [...state.components];
163
+ const [removed] = newComponents.splice(fromIndex, 1);
164
+ newComponents.splice(toIndex, 0, removed);
165
+ return { components: newComponents };
166
+ }),
167
+
168
+ clearComponents: () => set({ components: [], selectedComponent: null }),
169
+
170
+ saveTemplate: (name) =>
171
+ set((state) => {
172
+ const newTemplate: EmailTemplate = {
173
+ id: Date.now().toString(),
174
+ name,
175
+ components: state.components,
176
+ createdAt: new Date().toISOString(),
177
+ };
178
+ return {
179
+ templates: [...state.templates, newTemplate],
180
+ currentTemplate: newTemplate,
181
+ };
182
+ }),
183
+
184
+ loadTemplate: (template) =>
185
+ set({
186
+ components: template.components,
187
+ currentTemplate: template,
188
+ selectedComponent: null,
189
+ }),
190
+
191
+ deleteTemplate: (id) =>
192
+ set((state) => ({
193
+ templates: state.templates.filter((t) => t.id !== id),
194
+ currentTemplate:
195
+ state.currentTemplate?.id === id ? null : state.currentTemplate,
196
+ })),
197
+ }));
198
+
package/package.json ADDED
@@ -0,0 +1,76 @@
1
+ {
2
+ "name": "email-builder-pro",
3
+ "version": "1.0.0",
4
+ "description": "A fully functional drag-and-drop email template builder with React and Next.js",
5
+ "main": "src/index.tsx",
6
+ "types": "src/index.tsx",
7
+ "files": [
8
+ "src",
9
+ "components",
10
+ "lib",
11
+ "types",
12
+ "app/globals.css",
13
+ "tailwind.config.js",
14
+ "postcss.config.js",
15
+ "README.md",
16
+ "INTEGRATION.md",
17
+ "PUBLISH.md"
18
+ ],
19
+ "keywords": [
20
+ "email",
21
+ "email-builder",
22
+ "drag-drop",
23
+ "react",
24
+ "nextjs",
25
+ "template",
26
+ "email-template",
27
+ "react-email",
28
+ "visual-editor"
29
+ ],
30
+ "author": "pranaykodam@gmail.com",
31
+ "license": "MIT",
32
+ "repository": {
33
+ "type": "git",
34
+ "url": "https://github.com/pranay213/email-builder.git"
35
+ },
36
+ "private": false,
37
+ "scripts": {
38
+ "dev": "next dev",
39
+ "build": "next build",
40
+ "start": "next start",
41
+ "lint": "next lint",
42
+ "email:dev": "email dev",
43
+ "email:export": "email export",
44
+ "prepublishOnly": "npm run build",
45
+ "publish:package": "bash scripts/publish.sh",
46
+ "publish": "npm publish --access public"
47
+ },
48
+ "dependencies": {
49
+ "@react-email/components": "^1.0.2",
50
+ "next": "^14.0.0",
51
+ "react": "^18.2.0",
52
+ "react-dom": "^18.2.0",
53
+ "react-dnd": "^16.0.1",
54
+ "react-dnd-html5-backend": "^16.0.1",
55
+ "lucide-react": "^0.294.0",
56
+ "zustand": "^4.4.7"
57
+ },
58
+ "peerDependencies": {
59
+ "react": "^18.2.0",
60
+ "react-dom": "^18.2.0",
61
+ "next": "^14.0.0"
62
+ },
63
+ "devDependencies": {
64
+ "@react-email/preview-server": "^5.1.0",
65
+ "@types/node": "^20.10.0",
66
+ "@types/react": "^18.2.0",
67
+ "@types/react-dom": "^18.2.0",
68
+ "react-email": "^5.1.0",
69
+ "typescript": "^5.3.0",
70
+ "tailwindcss": "^3.4.0",
71
+ "postcss": "^8.4.32",
72
+ "autoprefixer": "^10.4.16",
73
+ "eslint": "^8.55.0",
74
+ "eslint-config-next": "^14.0.0"
75
+ }
76
+ }
@@ -0,0 +1,8 @@
1
+ module.exports = {
2
+ plugins: {
3
+ tailwindcss: {},
4
+ autoprefixer: {},
5
+ },
6
+ };
7
+
8
+
package/src/index.tsx ADDED
@@ -0,0 +1,42 @@
1
+ // Main entry point for the email-builder package
2
+ // This file exports all public APIs
3
+
4
+ 'use client';
5
+
6
+ import { DndProvider } from 'react-dnd';
7
+ import { HTML5Backend } from 'react-dnd-html5-backend';
8
+ import EmailBuilderComponent from '../components/EmailBuilder';
9
+
10
+ export interface EmailBuilderProps {
11
+ className?: string;
12
+ }
13
+
14
+ // Default export - Main EmailBuilder component
15
+ export default function EmailBuilder(props?: EmailBuilderProps) {
16
+ return (
17
+ <DndProvider backend={HTML5Backend}>
18
+ <div className={props?.className}>
19
+ <EmailBuilderComponent />
20
+ </div>
21
+ </DndProvider>
22
+ );
23
+ }
24
+
25
+ // Named exports for advanced usage
26
+ export { default as EmailBuilderCore } from '../components/EmailBuilder';
27
+ export { default as Canvas } from '../components/Canvas';
28
+ export { default as ComponentPalette } from '../components/ComponentPalette';
29
+ export { default as PropertiesPanel } from '../components/PropertiesPanel';
30
+ export { default as PreviewPanel } from '../components/PreviewPanel';
31
+ export { default as Toolbar } from '../components/Toolbar';
32
+ export { default as TemplateManager } from '../components/TemplateManager';
33
+ export { default as ComponentRenderer } from '../components/ComponentRenderer';
34
+ export { default as ImageUpload } from '../components/ImageUpload';
35
+ export { default as NumberInput } from '../components/NumberInput';
36
+
37
+ // Export types
38
+ export * from '../types/email';
39
+
40
+ // Export store hook
41
+ export { useEmailBuilder } from '../lib/store';
42
+