@one-million-lines/email-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.
Files changed (51) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/LICENSE +21 -0
  3. package/README.md +211 -0
  4. package/dist/App.d.ts +1 -0
  5. package/dist/core/aiActions.d.ts +52 -0
  6. package/dist/core/plugins.d.ts +26 -0
  7. package/dist/core/renderer.d.ts +2 -0
  8. package/dist/core/theme.d.ts +3 -0
  9. package/dist/core/types.d.ts +150 -0
  10. package/dist/core/utils.d.ts +3 -0
  11. package/dist/core/validation.d.ts +810 -0
  12. package/dist/editor/Canvas.d.ts +1 -0
  13. package/dist/editor/LeftSidebar.d.ts +1 -0
  14. package/dist/editor/RecommendationsPanel.d.ts +6 -0
  15. package/dist/editor/RightSidebar.d.ts +1 -0
  16. package/dist/editor/TemplatesModal.d.ts +6 -0
  17. package/dist/editor/TopBar.d.ts +1 -0
  18. package/dist/email-builder.cjs +80 -0
  19. package/dist/email-builder.cjs.map +1 -0
  20. package/dist/email-builder.js +8766 -0
  21. package/dist/email-builder.js.map +1 -0
  22. package/dist/index.d.ts +35 -0
  23. package/dist/modules/basic.d.ts +2 -0
  24. package/dist/modules/content.d.ts +2 -0
  25. package/dist/modules/cta.d.ts +2 -0
  26. package/dist/modules/defaultModules.d.ts +14 -0
  27. package/dist/modules/ecommerce.d.ts +2 -0
  28. package/dist/modules/feature.d.ts +2 -0
  29. package/dist/modules/footer.d.ts +2 -0
  30. package/dist/modules/header.d.ts +2 -0
  31. package/dist/modules/helpers.d.ts +89 -0
  32. package/dist/modules/menu.d.ts +2 -0
  33. package/dist/modules/registry.d.ts +24 -0
  34. package/dist/modules/social.d.ts +2 -0
  35. package/dist/modules/transactional.d.ts +2 -0
  36. package/dist/plugins/imageUploader/index.d.ts +36 -0
  37. package/dist/recommendations/logic.d.ts +99 -0
  38. package/dist/store/emailStore.d.ts +42 -0
  39. package/dist/styles.css +1 -0
  40. package/dist/templates/abandonedCart.d.ts +3 -0
  41. package/dist/templates/blogDigest.d.ts +3 -0
  42. package/dist/templates/eventInvite.d.ts +3 -0
  43. package/dist/templates/flashSale.d.ts +3 -0
  44. package/dist/templates/index.d.ts +2 -0
  45. package/dist/templates/orderReceipt.d.ts +3 -0
  46. package/dist/templates/productLaunch.d.ts +3 -0
  47. package/dist/templates/registry.d.ts +25 -0
  48. package/dist/templates/weeklyNewsletter.d.ts +3 -0
  49. package/dist/templates/welcomeOnboarding.d.ts +3 -0
  50. package/dist/themes/defaultThemes.d.ts +7 -0
  51. package/package.json +91 -0
package/CHANGELOG.md ADDED
@@ -0,0 +1,25 @@
1
+ # Changelog
2
+
3
+ All notable changes to this package are documented here.
4
+ This project adheres to [Semantic Versioning](https://semver.org/) and the
5
+ [Keep a Changelog](https://keepachangelog.com/) format.
6
+
7
+ ## [Unreleased]
8
+
9
+ ### Added
10
+ - npm library build (`dist/email-builder.js` ESM, `dist/email-builder.cjs` CommonJS).
11
+ - Generated TypeScript declarations (`dist/index.d.ts` + per-module `.d.ts`).
12
+ - Compiled stylesheet at `dist/styles.css`, imported via `openpostcards-builder/styles.css`.
13
+ - `exports` map, `files` allowlist, and publishing metadata in `package.json`.
14
+ - Library-only stylesheet (`src/lib.css`) that omits app `html/body/#root` globals.
15
+
16
+ ### Changed
17
+ - `react` and `react-dom` are now **peer dependencies** (no longer bundled).
18
+ - Runtime dependencies (`@dnd-kit/*`, `lucide-react`, `zod`, `zustand`) are
19
+ externalized from the bundle and declared as `dependencies`.
20
+
21
+ ## [0.1.0]
22
+
23
+ ### Added
24
+ - Initial visual email builder: JSON document model, modules, templates, themes,
25
+ table-based HTML renderer, plugin hooks, React component, and vanilla factory.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Alex Rada
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,211 @@
1
+ # OpenPostcards AI Builder
2
+
3
+ OpenPostcards AI Builder is a visual email builder for composing modular marketing emails from a JSON document model and exporting table-based HTML. It can run as a standalone app or be embedded into other products through a React component or a vanilla JavaScript wrapper.
4
+
5
+ ## What it does
6
+
7
+ It gives users a drag-and-drop editor with reusable modules, themes, HTML export, and plugin hooks for extending the builder.
8
+
9
+ ## Why it exists
10
+
11
+ Marketing teams often need a reusable editor that produces email-safe HTML without tying the editor to a specific CMS or backend. This project separates the editing experience, the JSON document model, and the final HTML renderer so it can be embedded elsewhere.
12
+
13
+ ## Features
14
+
15
+ - Visual editor with top bar, left sidebar, canvas, and right sidebar
16
+ - JSON-first email document model
17
+ - Built-in modules, templates, and themes
18
+ - Table-based HTML rendering for email output
19
+ - Local autosave via Zustand store
20
+ - React component API and vanilla JS factory
21
+ - Plugin registration for modules and integrations
22
+ - AI action pipeline that validates structured output with Zod before applying changes
23
+
24
+ ## How it works
25
+
26
+ 1. The app registers default modules and starter templates at load time.
27
+ 2. The email document lives in a Zustand store with import, export, and autosave support.
28
+ 3. The canvas and sidebars edit that shared document model.
29
+ 4. `renderEmailHtml()` converts the JSON document into table-based email HTML.
30
+ 5. Embedders can mount the editor with `EmailBuilder` or `createEmailBuilder()` and subscribe to document or HTML changes.
31
+
32
+ ## Tech stack
33
+
34
+ - React
35
+ - TypeScript
36
+ - Vite
37
+ - Zustand
38
+ - Zod
39
+ - dnd-kit
40
+
41
+ ## Project structure
42
+
43
+ ```text
44
+ src/
45
+ core/ document types, renderer, validation, AI actions, plugins
46
+ editor/ top bar, sidebars, canvas
47
+ modules/ module registry and built-in modules
48
+ recommendations/ recommendation and fallback logic
49
+ store/ editor state and persistence
50
+ templates/ built-in email templates
51
+ themes/ theme definitions
52
+ plugins/ extension points, including image uploader helpers
53
+ index.ts public API for embedding
54
+ ```
55
+
56
+ ## Install
57
+
58
+ ```bash
59
+ npm install openpostcards-builder react react-dom
60
+ ```
61
+
62
+ `react` and `react-dom` (v18 or v19) are **peer dependencies** — install them in
63
+ your app; they are never bundled into the package.
64
+
65
+ > **MANUAL ACTION REQUIRED:** `openpostcards-builder` is a placeholder package
66
+ > name. Check availability and choose/reserve your own npm name before publishing,
67
+ > then update `package.json` `name`, `repository`, `homepage`, and `bugs`.
68
+
69
+ ## Getting started
70
+
71
+ ```bash
72
+ git clone <repo-url>
73
+ cd email-builder
74
+ npm install
75
+ npm run dev
76
+ ```
77
+
78
+ Open `http://localhost:5315`.
79
+
80
+ ## Configuration
81
+
82
+ This project does not require environment variables for local development.
83
+
84
+ The production build uses `/demo/email-builder/` as the Vite base path.
85
+
86
+ ## Usage
87
+
88
+ React:
89
+
90
+ ```tsx
91
+ import { EmailBuilder } from "openpostcards-builder";
92
+ import "openpostcards-builder/styles.css";
93
+
94
+ <EmailBuilder
95
+ initialDocument={emailJson}
96
+ onChange={(doc) => console.log(doc)}
97
+ onExportHtml={(html) => console.log(html)}
98
+ />;
99
+ ```
100
+
101
+ Vanilla JS:
102
+
103
+ ```ts
104
+ import { createEmailBuilder } from "openpostcards-builder";
105
+ import "openpostcards-builder/styles.css";
106
+
107
+ const instance = createEmailBuilder({
108
+ container: document.getElementById("builder")!,
109
+ initialDocument,
110
+ onChange(doc) {
111
+ console.log(doc);
112
+ },
113
+ });
114
+
115
+ // instance.getDocument(); instance.exportHtml(); instance.exportJson(); instance.destroy();
116
+ ```
117
+
118
+ Always import the stylesheet once per app: `import "openpostcards-builder/styles.css";`
119
+
120
+ ### Framework integration
121
+
122
+ Thin wrappers for each framework live in [`examples/`](./examples):
123
+
124
+ - **React** — `examples/react/App.jsx`
125
+ - **Vue 3** — `examples/vue/EmailBuilder.vue`
126
+ - **Angular** — `examples/angular/email-builder.component.ts`
127
+ - **Plain JS** — `examples/plain/index.html`
128
+
129
+ Vue and Angular mount the React-based editor through the framework-neutral
130
+ `createEmailBuilder()` factory (`getDocument` / `exportHtml` / `exportJson` /
131
+ `destroy`). React and ReactDOM remain peer dependencies in all cases.
132
+
133
+ ### Styling & isolation
134
+
135
+ The stylesheet at `openpostcards-builder/styles.css` includes Tailwind
136
+ **Preflight**, a global CSS reset that normalizes `margin`, `box-sizing`,
137
+ headings, lists, etc. When embedding into an existing design system this can
138
+ affect host styles. Mitigations:
139
+
140
+ - Load the editor in a dedicated route/page where the reset is acceptable, or
141
+ - Scope/encapsulate it (e.g., render inside an iframe or a Shadow DOM host), or
142
+ - Build your own Tailwind stylesheet without Preflight if your app already
143
+ provides resets.
144
+
145
+ The editor root uses `h-screen w-screen`; place it in a full-size container.
146
+
147
+ ### Server-side rendering
148
+
149
+ The package does not access `window`/`document` at import time, so it is safe to
150
+ import in SSR frameworks. The editor itself is client-only — render it in an
151
+ effect, or dynamically import it with SSR disabled in Next.js:
152
+
153
+ ```tsx
154
+ import dynamic from "next/dynamic";
155
+
156
+ const EmailBuilder = dynamic(
157
+ () => import("openpostcards-builder").then((m) => m.EmailBuilder),
158
+ { ssr: false }
159
+ );
160
+ ```
161
+
162
+ ## Development
163
+
164
+ ```bash
165
+ npm run dev # standalone app
166
+ npm run build # build the library into dist/ (ESM + CJS + CSS + d.ts)
167
+ npm test # package-consumption tests (jsdom + React)
168
+ npm run lint # eslint
169
+ npm run validate:pack # build + npm pack --dry-run
170
+ npm run build:demo # build the standalone demo app
171
+ ```
172
+
173
+ ## Building & publishing
174
+
175
+ ```bash
176
+ npm run build # produces dist/
177
+ npm pack --dry-run # inspect the tarball contents
178
+ npm pack # create the .tgz to test in a consumer app
179
+ ```
180
+
181
+ To publish (run manually):
182
+
183
+ ```bash
184
+ # MANUAL ACTION REQUIRED — choose a real package name first, then:
185
+ npm login
186
+ npm publish --access public
187
+ ```
188
+
189
+ `prepublishOnly` rebuilds the library automatically before publish.
190
+
191
+ ## Roadmap
192
+
193
+ - Add persistence adapters beyond local storage
194
+ - Add more production-ready media upload integrations
195
+ - Add automated tests for document validation and HTML export
196
+ - Add richer framework wrappers and packaging docs
197
+
198
+ ## Contributing
199
+
200
+ This project is public and open for collaboration. If you’re interested in contributing, improving the project, or discussing ideas, feel free to reach out.
201
+
202
+ LinkedIn: https://linkedin.com/in/alexrada
203
+
204
+ 1. Fork the repository
205
+ 2. Create a new branch
206
+ 3. Make your changes
207
+ 4. Open a pull request
208
+
209
+ ## License
210
+
211
+ This project is licensed under the MIT License. See [LICENSE](./LICENSE).
package/dist/App.d.ts ADDED
@@ -0,0 +1 @@
1
+ export declare function App(): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,52 @@
1
+ import { EmailDocument, EmailModule, Theme } from '../core/types';
2
+ export type AIAction = {
3
+ type: "update_meta";
4
+ patch: Partial<EmailDocument["meta"]>;
5
+ } | {
6
+ type: "update_settings";
7
+ patch: Partial<EmailDocument["settings"]>;
8
+ } | {
9
+ type: "apply_theme";
10
+ theme: Theme;
11
+ } | {
12
+ type: "insert_module";
13
+ index?: number;
14
+ module: EmailModule;
15
+ } | {
16
+ type: "delete_module";
17
+ moduleId: string;
18
+ } | {
19
+ type: "update_module";
20
+ moduleId: string;
21
+ patch: Partial<EmailModule>;
22
+ } | {
23
+ type: "update_element";
24
+ moduleId: string;
25
+ elementId: string;
26
+ patch: Record<string, unknown>;
27
+ };
28
+ export interface AIRequest {
29
+ task: "create_email" | "rewrite_text" | "generate_subject" | "generate_preview" | "translate" | "adapt_tone" | "apply_theme";
30
+ document?: EmailDocument;
31
+ instruction?: string;
32
+ context?: Record<string, unknown>;
33
+ }
34
+ export interface AIResponse {
35
+ actions?: AIAction[];
36
+ document?: EmailDocument;
37
+ text?: string;
38
+ }
39
+ export interface AIProvider {
40
+ generate(req: AIRequest): Promise<AIResponse>;
41
+ }
42
+ export declare function applyAIActions(doc: EmailDocument, actions: AIAction[]): EmailDocument;
43
+ /** Validate an AI-returned document before applying. */
44
+ export declare function validateAIDocument(doc: unknown): {
45
+ ok: true;
46
+ document: EmailDocument;
47
+ } | {
48
+ ok: false;
49
+ error: string;
50
+ };
51
+ /** Mock AI provider for demos and tests. Returns deterministic patches. */
52
+ export declare const mockAIProvider: AIProvider;
@@ -0,0 +1,26 @@
1
+ import { ModuleDefinition } from '../modules/registry';
2
+ import { Theme } from './types';
3
+ import { AIProvider } from './aiActions';
4
+ export interface AssetProvider {
5
+ upload: (file: File) => Promise<{
6
+ url: string;
7
+ alt?: string;
8
+ }>;
9
+ }
10
+ export interface BuilderHandle {
11
+ registerModule: (def: ModuleDefinition) => void;
12
+ registerTheme: (theme: Theme) => void;
13
+ registerAssetProvider: (provider: AssetProvider) => void;
14
+ setAIProvider: (provider: AIProvider) => void;
15
+ }
16
+ export type PluginType = "modules" | "themes" | "asset-provider" | "ai-provider";
17
+ export interface Plugin {
18
+ name: string;
19
+ type: PluginType;
20
+ setup: (builder: BuilderHandle) => void;
21
+ }
22
+ export declare const builder: BuilderHandle;
23
+ export declare function registerPlugin(plugin: Plugin): void;
24
+ export declare function getRegisteredThemes(): Theme[];
25
+ export declare function getAssetProvider(): AssetProvider | null;
26
+ export declare function getAIProvider(): AIProvider | null;
@@ -0,0 +1,2 @@
1
+ import { EmailDocument } from './types';
2
+ export declare function renderEmailHtml(doc: EmailDocument): string;
@@ -0,0 +1,3 @@
1
+ import { Theme } from './types';
2
+ export declare function resolveToken(value: unknown, theme: Theme): unknown;
3
+ export declare function resolveStyle(style: Record<string, unknown> | undefined | null, theme: Theme): Record<string, unknown>;
@@ -0,0 +1,150 @@
1
+ export type ElementType = "text" | "image" | "button" | "spacer" | "divider" | "productGrid";
2
+ export interface BaseStyle {
3
+ paddingTop?: number;
4
+ paddingBottom?: number;
5
+ paddingLeft?: number;
6
+ paddingRight?: number;
7
+ backgroundColor?: string;
8
+ align?: "left" | "center" | "right";
9
+ borderRadius?: number;
10
+ border?: string;
11
+ hideOn?: "mobile" | "desktop";
12
+ /**
13
+ * Per-breakpoint overrides applied via @media (max-width:600px).
14
+ * Any field accepted by the owning element's style can be placed here.
15
+ */
16
+ mobile?: Record<string, unknown>;
17
+ }
18
+ export interface TextElement {
19
+ id: string;
20
+ type: "text";
21
+ role?: "headline" | "subheadline" | "body" | "caption";
22
+ content: string;
23
+ style?: BaseStyle & {
24
+ fontFamily?: string;
25
+ fontSize?: number;
26
+ lineHeight?: number;
27
+ letterSpacing?: number;
28
+ fontWeight?: number | string;
29
+ color?: string;
30
+ link?: string;
31
+ };
32
+ }
33
+ export interface ImageElement {
34
+ id: string;
35
+ type: "image";
36
+ src: string;
37
+ alt?: string;
38
+ link?: string;
39
+ style?: BaseStyle & {
40
+ width?: number;
41
+ height?: number;
42
+ };
43
+ }
44
+ export interface ButtonElement {
45
+ id: string;
46
+ type: "button";
47
+ label: string;
48
+ link: string;
49
+ style?: BaseStyle & {
50
+ backgroundColor?: string;
51
+ color?: string;
52
+ fontSize?: number;
53
+ fontFamily?: string;
54
+ fontWeight?: number | string;
55
+ };
56
+ }
57
+ export interface SpacerElement {
58
+ id: string;
59
+ type: "spacer";
60
+ height: number;
61
+ }
62
+ export interface DividerElement {
63
+ id: string;
64
+ type: "divider";
65
+ style?: {
66
+ color?: string;
67
+ thickness?: number;
68
+ paddingTop?: number;
69
+ paddingBottom?: number;
70
+ };
71
+ }
72
+ export interface Product {
73
+ id: string;
74
+ image: string;
75
+ imageAlt?: string;
76
+ name: string;
77
+ oldPrice?: string;
78
+ finalPrice: string;
79
+ description?: string;
80
+ link?: string;
81
+ buttonLabel?: string;
82
+ }
83
+ export interface ProductGridElement {
84
+ id: string;
85
+ type: "productGrid";
86
+ products: Product[];
87
+ columns: 1 | 2 | 3;
88
+ showOldPrice: boolean;
89
+ showButton: boolean;
90
+ showDescription: boolean;
91
+ buttonLabel?: string;
92
+ style?: BaseStyle & {
93
+ nameColor?: string;
94
+ finalPriceColor?: string;
95
+ oldPriceColor?: string;
96
+ buttonBackgroundColor?: string;
97
+ buttonColor?: string;
98
+ gap?: number;
99
+ cardBackgroundColor?: string;
100
+ borderRadius?: number;
101
+ align?: "left" | "center" | "right";
102
+ };
103
+ }
104
+ export type EmailElement = TextElement | ImageElement | ButtonElement | SpacerElement | DividerElement | ProductGridElement;
105
+ export interface EmailModule {
106
+ id: string;
107
+ type: string;
108
+ name: string;
109
+ style?: BaseStyle;
110
+ children: EmailElement[];
111
+ /** Free-form per-module data used by plugins (e.g. `recommendations` logic). */
112
+ data?: Record<string, unknown>;
113
+ }
114
+ export interface ThemeTokens {
115
+ colors: Record<string, string>;
116
+ fonts: Record<string, string>;
117
+ spacing: Record<string, number>;
118
+ radius: Record<string, number>;
119
+ }
120
+ export interface Theme {
121
+ id: string;
122
+ name: string;
123
+ tokens: ThemeTokens;
124
+ }
125
+ export interface EmailSettings {
126
+ width: number;
127
+ backgroundColor: string;
128
+ contentBackgroundColor: string;
129
+ }
130
+ export interface EmailMeta {
131
+ name: string;
132
+ previewText: string;
133
+ }
134
+ export interface EmailDocument {
135
+ version: string;
136
+ meta: EmailMeta;
137
+ theme: Theme;
138
+ settings: EmailSettings;
139
+ modules: EmailModule[];
140
+ }
141
+ export type Selection = {
142
+ kind: "email";
143
+ } | {
144
+ kind: "module";
145
+ moduleId: string;
146
+ } | {
147
+ kind: "element";
148
+ moduleId: string;
149
+ elementId: string;
150
+ } | null;
@@ -0,0 +1,3 @@
1
+ export declare function uid(prefix?: string): string;
2
+ export declare function escapeHtml(s: string): string;
3
+ export declare function safeUrl(url: string | undefined): string;