@vitrina/email-builder 0.1.4

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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Vitrina (VitrinaDev)
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,150 @@
1
+ # @vitrina/email-builder
2
+
3
+ [![npm](https://img.shields.io/npm/v/@vitrina/email-builder.svg)](https://www.npmjs.com/package/@vitrina/email-builder)
4
+ [![license](https://img.shields.io/npm/l/@vitrina/email-builder.svg)](./LICENSE)
5
+
6
+ An **Unlayer-style drag-and-drop email builder** for React. Rows → weighted
7
+ Columns → Blocks (text, heading, button, image, divider, spacer, social, menu,
8
+ HTML). Inline rich-text, undo/redo, desktop/mobile preview. It edits a plain
9
+ `EmailDesign` JSON object and compiles it to **email-safe table HTML** — inline
10
+ styles, forced-light rendering, fluid columns that stack on phones, and Outlook
11
+ fallbacks.
12
+
13
+ - **Framework-agnostic** — one `<EmailBuilder value onChange />` component.
14
+ - **Tailwind + shadcn friendly** — styled entirely with utility classes + your CSS-var theme tokens. No stylesheet to import.
15
+ - **i18n-free** — every visible string is injected via a `labels` prop.
16
+ - **Merge-tag aware** — `{{name}}`-style tags pass through untouched, for your own per-recipient rendering.
17
+ - **Tiny** — no editor engine bundled; only `highlight.js` + `lucide-react` as deps, React as a peer.
18
+
19
+ > Built by [Vitrina](https://github.com/VitrinaDev) for its campaigns product and extracted as a standalone package.
20
+
21
+ ## Install
22
+
23
+ ```bash
24
+ npm i @vitrina/email-builder
25
+ # or: pnpm add @vitrina/email-builder / yarn add @vitrina/email-builder
26
+ ```
27
+
28
+ Peer deps: **React ≥ 19** and `react-dom`.
29
+
30
+ ## Quick start
31
+
32
+ ```tsx
33
+ import { useState } from 'react';
34
+ import { EmailBuilder, compileDesign, defaultDesign, type EmailDesign } from '@vitrina/email-builder';
35
+
36
+ export function MyEditor() {
37
+ const [design, setDesign] = useState<EmailDesign>(() => defaultDesign());
38
+
39
+ return (
40
+ <div style={{ height: 640 }}>
41
+ <EmailBuilder
42
+ value={design}
43
+ onChange={setDesign}
44
+ uploadImage={async (file) => uploadAndReturnPublicUrl(file)} // optional
45
+ socialIconBase="https://cdn.example.com/social-icons" // optional
46
+ />
47
+ </div>
48
+ );
49
+ }
50
+
51
+ // When you save/send — compileDesign returns the <body> inner HTML. Wrap it in
52
+ // your own shell (doctype/head + compliance footer) and render merge tags
53
+ // server-side per recipient.
54
+ const bodyHtml = compileDesign(design, { socialIconBase: 'https://cdn.example.com/social-icons' });
55
+ ```
56
+
57
+ ## Styling requirements
58
+
59
+ The builder renders **Tailwind utility classes** and reads **shadcn/ui-style CSS
60
+ variables** (`--primary`, `--muted-foreground`, `--border`, `--popover`,
61
+ `--ring`, `--destructive`, …). Any shadcn base provides them.
62
+
63
+ Because the classes live inside the compiled package, tell Tailwind to scan it.
64
+
65
+ **Tailwind v4** (`globals.css`):
66
+
67
+ ```css
68
+ @import "tailwindcss";
69
+ @source "../node_modules/@vitrina/email-builder/dist";
70
+ ```
71
+
72
+ **Tailwind v3** (`tailwind.config`):
73
+
74
+ ```js
75
+ content: ['./src/**/*.{ts,tsx}', './node_modules/@vitrina/email-builder/dist/**/*.js'],
76
+ ```
77
+
78
+ The HTML block's code editor picks up syntax colors from any highlight.js theme
79
+ you happen to load — optional; it degrades to monochrome without one.
80
+
81
+ ## API
82
+
83
+ | Export | What |
84
+ | --- | --- |
85
+ | `EmailBuilder` | The editor. Props below. |
86
+ | `compileDesign(design, opts?)` | `EmailDesign` → email-safe body HTML string. `opts.socialIconBase` for hosted social icons. |
87
+ | `defaultDesign()` | A pleasant starter design. |
88
+ | `blockCount(design)` | Total blocks (e.g. to gate "next"). |
89
+ | `MOBILE_STACK_CSS` | Optional `<style>` for your `<head>` (extra mobile polish). |
90
+ | `DEFAULT_LABELS`, `BuilderLabels` | The English label bag + its type. |
91
+ | types | `EmailDesign`, `EmailSettings`, `Block`, `BlockType`, `Row` |
92
+
93
+ ### `<EmailBuilder>` props
94
+
95
+ | Prop | Type | |
96
+ | --- | --- | --- |
97
+ | `value` | `EmailDesign` | required — the design (controlled) |
98
+ | `onChange` | `(d: EmailDesign) => void` | required |
99
+ | `labels?` | `Partial<BuilderLabels>` | override any string (defaults are English) |
100
+ | `uploadImage?` | `(file: File) => Promise<string>` | host-provided uploader; without it images are URL-only |
101
+ | `socialIconBase?` | `string` | absolute base for hosted icons — `<base>/<slug>.png`; omit for a colored-chip fallback |
102
+ | `height?` | `number \| string` | editor height (default `640`) |
103
+
104
+ ## The compile contract
105
+
106
+ `compileDesign` emits the `<body>` **inner** HTML only. **You** own the outer
107
+ shell — doctype/`<head>`, and any compliance footer (unsubscribe, physical
108
+ address) your emails need. Merge tags like `{{name}}` are left verbatim so you
109
+ can render them per recipient at send time.
110
+
111
+ Rules the output follows so it renders across clients:
112
+
113
+ - Table-based layout, 100% inline styles (no `<style>`/media-queries in the body).
114
+ - **Forced light** friendliness — pair with `<meta name="color-scheme" content="light only">` in your head to avoid dark-mode inversion breaking your colors.
115
+ - **Fluid columns** — multi-column rows use the inline-block "hybrid" pattern, so they **stack on phones with no media query** (Gmail included), with an `[if mso]` ghost table keeping Outlook side-by-side.
116
+ - **Images** carry a `width` attribute (Outlook sizes off it) + `max-width:100%`.
117
+
118
+ ### Email-client notes
119
+
120
+ | | Gmail | Apple Mail | Outlook (desktop) |
121
+ | --- | --- | --- | --- |
122
+ | Core layout, buttons, images | ✅ | ✅ | ✅ |
123
+ | Column stacking on mobile | ✅ | ✅ | n/a |
124
+ | `border-radius` (rounded buttons/icons) | ✅ | ✅ | ⬜ renders square |
125
+ | Row **background images** | ✅ | ✅ | ⬜ needs VML |
126
+
127
+ Social icons need publicly-hosted PNGs at `socialIconBase` (Gmail/Outlook strip
128
+ SVG). Slugs: `instagram facebook whatsapp x twitter linkedin youtube tiktok
129
+ pinterest snapchat telegram messenger reddit spotify threads github vimeo tumblr
130
+ discord email website`.
131
+
132
+ ## Development
133
+
134
+ ```bash
135
+ pnpm install
136
+ pnpm test # vitest — compiler + model unit tests
137
+ pnpm typecheck
138
+ pnpm lint
139
+ pnpm build # tsup → dist (ESM + d.ts)
140
+
141
+ # runnable playground (see examples/basic):
142
+ cd examples/basic && pnpm install && pnpm dev
143
+ ```
144
+
145
+ See [CONTRIBUTING.md](./CONTRIBUTING.md) for the full workflow and
146
+ [CHANGELOG.md](./CHANGELOG.md) for release history.
147
+
148
+ ## License
149
+
150
+ [MIT](./LICENSE) © Vitrina
@@ -0,0 +1,243 @@
1
+ import * as react from 'react';
2
+
3
+ type BlockAlign = 'left' | 'center' | 'right';
4
+ interface BlockBase {
5
+ id: string;
6
+ padding: number;
7
+ }
8
+ interface TextBlock extends BlockBase {
9
+ type: 'text';
10
+ html: string;
11
+ align: BlockAlign;
12
+ fontSize: number;
13
+ color: string;
14
+ lineHeight: number;
15
+ }
16
+ interface HeadingBlock extends BlockBase {
17
+ type: 'heading';
18
+ html: string;
19
+ level: 1 | 2 | 3;
20
+ align: BlockAlign;
21
+ color: string;
22
+ }
23
+ interface ButtonBlock extends BlockBase {
24
+ type: 'button';
25
+ label: string;
26
+ href: string;
27
+ align: BlockAlign;
28
+ background: string;
29
+ color: string;
30
+ radius: number;
31
+ fullWidth: boolean;
32
+ }
33
+ type LinkAction = 'url' | 'email' | 'phone';
34
+ interface ImageBlock extends BlockBase {
35
+ type: 'image';
36
+ src: string;
37
+ alt: string;
38
+ href: string;
39
+ linkAction: LinkAction;
40
+ align: BlockAlign;
41
+ width: number;
42
+ }
43
+ interface DividerBlock extends BlockBase {
44
+ type: 'divider';
45
+ color: string;
46
+ thickness: number;
47
+ }
48
+ interface SpacerBlock extends BlockBase {
49
+ type: 'spacer';
50
+ height: number;
51
+ }
52
+ interface SocialItem {
53
+ network: string;
54
+ url: string;
55
+ }
56
+ type IconStyle = 'circle' | 'rounded' | 'square';
57
+ interface SocialBlock extends BlockBase {
58
+ type: 'social';
59
+ align: BlockAlign;
60
+ size: number;
61
+ spacing: number;
62
+ iconStyle: IconStyle;
63
+ items: SocialItem[];
64
+ }
65
+ interface HtmlBlock extends BlockBase {
66
+ type: 'html';
67
+ html: string;
68
+ }
69
+ interface MenuItem {
70
+ label: string;
71
+ url: string;
72
+ }
73
+ interface MenuBlock extends BlockBase {
74
+ type: 'menu';
75
+ items: MenuItem[];
76
+ layout: 'horizontal' | 'vertical';
77
+ align: BlockAlign;
78
+ fontSize: number;
79
+ color: string;
80
+ gap: number;
81
+ }
82
+ type Block = TextBlock | HeadingBlock | ButtonBlock | ImageBlock | DividerBlock | SpacerBlock | SocialBlock | HtmlBlock | MenuBlock;
83
+ type BlockType = Block['type'];
84
+ interface Column {
85
+ id: string;
86
+ weight: number;
87
+ background: string;
88
+ paddingX: number;
89
+ paddingY: number;
90
+ blocks: Block[];
91
+ }
92
+ interface Row {
93
+ id: string;
94
+ background: string;
95
+ contentBackground: string;
96
+ backgroundImage: string;
97
+ paddingY: number;
98
+ gap: number;
99
+ columns: Column[];
100
+ }
101
+ interface EmailSettings {
102
+ backgroundColor: string;
103
+ contentWidth: number;
104
+ fontFamily: string;
105
+ textColor: string;
106
+ linkColor: string;
107
+ preheader: string;
108
+ }
109
+ interface EmailDesign {
110
+ version: 1;
111
+ settings: EmailSettings;
112
+ rows: Row[];
113
+ }
114
+ /** A pleasant starter so a fresh campaign is never a blank canvas. */
115
+ declare function defaultDesign(): EmailDesign;
116
+ /** Count blocks across every row/column — the wizard gates "next" on ≥1. */
117
+ declare function blockCount(design: EmailDesign): number;
118
+
119
+ interface BuilderLabels {
120
+ tabContent: string;
121
+ tabRows: string;
122
+ tabSettings: string;
123
+ blockNames: Record<BlockType, string>;
124
+ emailSettings: string;
125
+ rowSettings: string;
126
+ columnSettings: string;
127
+ columns: string;
128
+ back: string;
129
+ duplicate: string;
130
+ remove: string;
131
+ moveUp: string;
132
+ moveDown: string;
133
+ addRow: string;
134
+ empty: string;
135
+ dropRowHere: string;
136
+ undo: string;
137
+ redo: string;
138
+ preview: string;
139
+ desktop: string;
140
+ mobile: string;
141
+ edit: string;
142
+ bold: string;
143
+ italic: string;
144
+ underline: string;
145
+ strike: string;
146
+ link: string;
147
+ linkUrl: string;
148
+ bulletList: string;
149
+ numberList: string;
150
+ clearFormat: string;
151
+ insertField: string;
152
+ align: string;
153
+ fontSize: string;
154
+ textColor: string;
155
+ lineHeight: string;
156
+ level: string;
157
+ buttonLabel: string;
158
+ buttonHref: string;
159
+ buttonBg: string;
160
+ radius: string;
161
+ fullWidth: string;
162
+ imageUrl: string;
163
+ imageAlt: string;
164
+ imageHref: string;
165
+ imageWidth: string;
166
+ imageWidthHint: string;
167
+ uploadImage: string;
168
+ uploading: string;
169
+ dropImage: string;
170
+ color: string;
171
+ thickness: string;
172
+ height: string;
173
+ size: string;
174
+ textEditHint: string;
175
+ socialLinks: string;
176
+ socialAdd: string;
177
+ socialNetwork: string;
178
+ socialUrl: string;
179
+ iconSpacing: string;
180
+ htmlContent: string;
181
+ htmlHint: string;
182
+ menuItems: string;
183
+ menuAdd: string;
184
+ layout: string;
185
+ horizontal: string;
186
+ vertical: string;
187
+ itemGap: string;
188
+ action: string;
189
+ linkType: string;
190
+ openWebsite: string;
191
+ sendEmail: string;
192
+ callPhone: string;
193
+ linkValue: string;
194
+ rowBg: string;
195
+ contentBg: string;
196
+ backgroundImage: string;
197
+ paddingY: string;
198
+ paddingX: string;
199
+ blockGap: string;
200
+ columnBg: string;
201
+ containerPadding: string;
202
+ iconStyle: string;
203
+ styleCircle: string;
204
+ styleRounded: string;
205
+ styleSquare: string;
206
+ pageBg: string;
207
+ contentWidth: string;
208
+ font: string;
209
+ linkColor: string;
210
+ preheader: string;
211
+ preheaderHint: string;
212
+ }
213
+ declare const DEFAULT_LABELS: BuilderLabels;
214
+
215
+ interface EmailBuilderProps {
216
+ value: EmailDesign;
217
+ onChange: (design: EmailDesign) => void;
218
+ labels?: Partial<BuilderLabels>;
219
+ /** Host-provided uploader; without it the image block is URL-only. */
220
+ uploadImage?: (file: File) => Promise<string>;
221
+ /** Absolute base URL for hosted social icons (<base>/<slug>.png). Omit to
222
+ * use the built-in colored-chip fallback. */
223
+ socialIconBase?: string;
224
+ height?: number | string;
225
+ }
226
+ declare function EmailBuilder({ value, onChange, labels: labelOverrides, uploadImage, socialIconBase, height }: EmailBuilderProps): react.JSX.Element;
227
+
228
+ interface CompileOpts {
229
+ /** Absolute base URL for hosted social icons (<base>/<slug>.png). When
230
+ * omitted, social renders the self-contained colored-chip fallback. */
231
+ socialIconBase?: string;
232
+ }
233
+ /** Optional. Multi-column rows already stack on narrow screens via the fluid
234
+ * hybrid layout (no media query needed — Gmail included). This just widens
235
+ * the stacked columns to the full width on phones for a touch more polish;
236
+ * drop it into your email's <head> if you like (compileDesign emits body-only
237
+ * HTML, so the head belongs to the consumer's shell). */
238
+ declare const MOBILE_STACK_CSS = "<style>@media only screen and (max-width:600px){.eb-col{max-width:100%!important}}</style>";
239
+ /** Design → the body HTML stored in campaign.content.html. Full-bleed rows on
240
+ * the page background; the server appends the compliant footer at send time. */
241
+ declare function compileDesign(design: EmailDesign, opts?: CompileOpts): string;
242
+
243
+ export { type Block, type BlockType, type BuilderLabels, DEFAULT_LABELS, EmailBuilder, type EmailBuilderProps, type EmailDesign, type EmailSettings, MOBILE_STACK_CSS, type Row, blockCount, compileDesign, defaultDesign };