@one-million-lines/email-builder 0.2.2 → 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.
- package/CHANGELOG.md +51 -0
- package/README.md +43 -0
- package/dist/core/plugins.d.ts +43 -2
- package/dist/core/types.d.ts +31 -1
- package/dist/core/utils.d.ts +7 -0
- package/dist/core/validation.d.ts +8 -0
- package/dist/email-builder.cjs +16 -15
- package/dist/email-builder.cjs.map +1 -1
- package/dist/email-builder.js +2778 -1857
- package/dist/email-builder.js.map +1 -1
- package/dist/index.d.ts +37 -3
- package/dist/modules/helpers.d.ts +34 -2
- package/dist/plugins/mergeTags/state.d.ts +8 -0
- package/dist/plugins/productSearch/ProductSearchModal.d.ts +13 -0
- package/dist/plugins/productSearch/index.d.ts +45 -0
- package/dist/plugins/productSearch/state.d.ts +11 -0
- package/dist/plugins/productSearch/useProductSearch.d.ts +2 -0
- package/dist/plugins/voucherSelect/VoucherPanel.d.ts +10 -0
- package/dist/plugins/voucherSelect/index.d.ts +45 -0
- package/dist/plugins/voucherSelect/logic.d.ts +16 -0
- package/dist/plugins/voucherSelect/state.d.ts +24 -0
- package/dist/plugins/voucherSelect/useVouchers.d.ts +15 -0
- package/dist/styles.css +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,57 @@ All notable changes to this package are documented here.
|
|
|
4
4
|
This project adheres to [Semantic Versioning](https://semver.org/) and the
|
|
5
5
|
[Keep a Changelog](https://keepachangelog.com/) format.
|
|
6
6
|
|
|
7
|
+
## [0.4.0] — 2026-09-03
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- **Special link types** — links can now be tagged with a semantic `linkType`
|
|
12
|
+
(`"unsubscribe"`, `"view_in_browser"`, `"manage_preferences"`, `"user_profile"`).
|
|
13
|
+
The rendered HTML carries both a `data-link-type` attribute on the anchor element
|
|
14
|
+
**and** a placeholder href value (e.g. `{{unsubscribe_url}}`), giving backend
|
|
15
|
+
processors two independent ways to locate and replace per-recipient URLs.
|
|
16
|
+
- New `SpecialLinkType` union type and `SPECIAL_LINK_PLACEHOLDERS` map exported
|
|
17
|
+
from `core/types`.
|
|
18
|
+
- `linkType` field added to `ButtonElement` and `ImageElement` (top-level), and
|
|
19
|
+
to `TextElement.style` (alongside the existing `link`).
|
|
20
|
+
- `text()`, `button()`, and `muted()` helpers accept a `linkType` option.
|
|
21
|
+
- New `footerLinks(links, opts?)` helper creates a text element with multiple
|
|
22
|
+
inline `<a>` tags, each carrying `data-link-type` — ideal for footer lines
|
|
23
|
+
that combine "Unsubscribe · View in browser".
|
|
24
|
+
- All built-in footer modules updated to use `footerLinks()` and proper
|
|
25
|
+
`linkType` values instead of plain text.
|
|
26
|
+
- Right sidebar: **Link role** dropdown added below the Link URL field for
|
|
27
|
+
Text, Image, and Button elements. Selecting a role auto-fills the placeholder
|
|
28
|
+
URL.
|
|
29
|
+
- `safeUrl()` updated to pass through `{{...}}` placeholder URLs without
|
|
30
|
+
stripping them.
|
|
31
|
+
- Validation schema updated to accept `linkType` on image and button elements.
|
|
32
|
+
|
|
33
|
+
- **Merge tags / personalisation tokens** — dynamic content placeholders can
|
|
34
|
+
now be configured and inserted directly from the editor sidebar.
|
|
35
|
+
- New `MergeTag` interface: `{ attribute: string; title: string }`.
|
|
36
|
+
- New `mergeTags` prop on `<EmailBuilder>` (React) and `createEmailBuilder`
|
|
37
|
+
(vanilla) accepts an array of merge tag definitions.
|
|
38
|
+
- New `builder.registerMergeTags(tags)` method on `BuilderHandle` for plugin
|
|
39
|
+
authors.
|
|
40
|
+
- New `useMergeTagsStore` reactive store (`src/plugins/mergeTags/state.ts`).
|
|
41
|
+
- Right sidebar: **Insert merge tag** dropdown appears in the Text element
|
|
42
|
+
panel when merge tags are configured. Selecting one appends
|
|
43
|
+
`{attribute}` to the text content.
|
|
44
|
+
- `MergeTag` exported from the public API.
|
|
45
|
+
|
|
46
|
+
- **Template & block authoring guide** (`src/templates/TEMPLATE_GUIDE.md`) —
|
|
47
|
+
comprehensive, AI-agent-ready reference documenting:
|
|
48
|
+
- The full `EmailDocument` JSON schema
|
|
49
|
+
- All element types and their style options
|
|
50
|
+
- Module structure, categories, and naming conventions
|
|
51
|
+
- Theme tokens and how to reference them
|
|
52
|
+
- All helper functions with options tables
|
|
53
|
+
- Special link types and merge tags
|
|
54
|
+
- Step-by-step instructions for creating new templates and module definitions
|
|
55
|
+
- A complete template code example
|
|
56
|
+
- A commit checklist and AI agent prompt template
|
|
57
|
+
|
|
7
58
|
## [Unreleased]
|
|
8
59
|
|
|
9
60
|
### Added
|
package/README.md
CHANGED
|
@@ -155,6 +155,49 @@ response is validated with Zod before it is applied. See
|
|
|
155
155
|
[`src/ai/README.md`](./src/ai/README.md) and
|
|
156
156
|
[`backend/README.md`](./backend/README.md).
|
|
157
157
|
|
|
158
|
+
### Product search
|
|
159
|
+
|
|
160
|
+
Connect product cards to your catalog. When a product provider is configured, a
|
|
161
|
+
**Find** button appears on any product grid (and a search icon on each card).
|
|
162
|
+
Searching opens a modal, previews the returned product, and — on **Save** —
|
|
163
|
+
populates the card's fields (which stay fully editable afterwards).
|
|
164
|
+
|
|
165
|
+
```tsx
|
|
166
|
+
<EmailBuilder productEndpoint="http://localhost:3001/products/search" />
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
```ts
|
|
170
|
+
import { registerPlugin, productSearchPlugin } from "@one-million-lines/email-builder";
|
|
171
|
+
registerPlugin(productSearchPlugin({ endpoint: "http://localhost:3001/products/search" }));
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
The demo backend in [`backend/`](./backend) ships a `/products/search` endpoint
|
|
175
|
+
over a small in-memory catalog. Product fields are `title`, `final_price`,
|
|
176
|
+
`old_price` (optional), `description`, `link`, `image` and `stars` (optional).
|
|
177
|
+
See [`src/plugins/productSearch/README.md`](./src/plugins/productSearch/README.md)
|
|
178
|
+
for the wire protocol and response mapping.
|
|
179
|
+
|
|
180
|
+
### Voucher select
|
|
181
|
+
|
|
182
|
+
Let users pick a discount code from your backend instead of typing it. When a
|
|
183
|
+
voucher provider is configured, selecting a **voucher block** (the built-in
|
|
184
|
+
**Voucher Code** module, or any module with a `voucherCode` text element) shows a
|
|
185
|
+
**Select voucher** dropdown that fills the code. The list is fetched once and
|
|
186
|
+
cached (lazily, or eagerly with `preload`).
|
|
187
|
+
|
|
188
|
+
```tsx
|
|
189
|
+
<EmailBuilder voucherEndpoint="http://localhost:3001/vouchers" />
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
```ts
|
|
193
|
+
import { registerPlugin, voucherPlugin } from "@one-million-lines/email-builder";
|
|
194
|
+
registerPlugin(voucherPlugin({ endpoint: "http://localhost:3001/vouchers", preload: true }));
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
The demo backend ships a `GET /vouchers` endpoint. See
|
|
198
|
+
[`src/plugins/voucherSelect/README.md`](./src/plugins/voucherSelect/README.md)
|
|
199
|
+
for the wire protocol and response mapping.
|
|
200
|
+
|
|
158
201
|
### Styling & isolation
|
|
159
202
|
|
|
160
203
|
The stylesheet at `@one-million-lines/email-builder/styles.css` is designed to
|
package/dist/core/plugins.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ModuleDefinition } from '../modules/registry';
|
|
2
|
-
import { Theme } from './types';
|
|
2
|
+
import { Theme, MergeTag } from './types';
|
|
3
3
|
import { AIProvider } from './aiActions';
|
|
4
4
|
export interface AssetProvider {
|
|
5
5
|
upload: (file: File) => Promise<{
|
|
@@ -7,13 +7,52 @@ export interface AssetProvider {
|
|
|
7
7
|
alt?: string;
|
|
8
8
|
}>;
|
|
9
9
|
}
|
|
10
|
+
/**
|
|
11
|
+
* A single product returned by a {@link ProductProvider} search. Field names
|
|
12
|
+
* match the builder's `Product` model so results drop straight into a card.
|
|
13
|
+
*/
|
|
14
|
+
export interface ProductSearchResult {
|
|
15
|
+
name: string;
|
|
16
|
+
finalPrice: string;
|
|
17
|
+
oldPrice?: string;
|
|
18
|
+
description?: string;
|
|
19
|
+
link?: string;
|
|
20
|
+
image?: string;
|
|
21
|
+
imageAlt?: string;
|
|
22
|
+
stars?: number;
|
|
23
|
+
/** Optional external identifier echoed back from the backend. */
|
|
24
|
+
sku?: string;
|
|
25
|
+
}
|
|
26
|
+
export interface ProductProvider {
|
|
27
|
+
/** Look up a single product for a free-text query. Resolves null if none. */
|
|
28
|
+
search: (query: string) => Promise<ProductSearchResult | null>;
|
|
29
|
+
}
|
|
30
|
+
/** A discount/voucher entry returned by a {@link VoucherProvider}. */
|
|
31
|
+
export interface Voucher {
|
|
32
|
+
/** Stable identifier (used to remember the selection). */
|
|
33
|
+
id: string;
|
|
34
|
+
/** Human label shown in the select dropdown. */
|
|
35
|
+
title: string;
|
|
36
|
+
/** The code (or merge tag) inserted into the voucher block. */
|
|
37
|
+
code: string;
|
|
38
|
+
/** Optional longer description. */
|
|
39
|
+
description?: string;
|
|
40
|
+
}
|
|
41
|
+
export interface VoucherProvider {
|
|
42
|
+
/** Load the list of vouchers to choose from. */
|
|
43
|
+
list: () => Promise<Voucher[]>;
|
|
44
|
+
}
|
|
10
45
|
export interface BuilderHandle {
|
|
11
46
|
registerModule: (def: ModuleDefinition) => void;
|
|
12
47
|
registerTheme: (theme: Theme) => void;
|
|
13
48
|
registerAssetProvider: (provider: AssetProvider) => void;
|
|
49
|
+
registerProductProvider: (provider: ProductProvider) => void;
|
|
50
|
+
registerVoucherProvider: (provider: VoucherProvider) => void;
|
|
14
51
|
setAIProvider: (provider: AIProvider) => void;
|
|
52
|
+
/** Configure the list of merge tags available in the text element sidebar. */
|
|
53
|
+
registerMergeTags: (tags: MergeTag[]) => void;
|
|
15
54
|
}
|
|
16
|
-
export type PluginType = "modules" | "themes" | "asset-provider" | "ai-provider";
|
|
55
|
+
export type PluginType = "modules" | "themes" | "asset-provider" | "product-provider" | "voucher-provider" | "ai-provider";
|
|
17
56
|
export interface Plugin {
|
|
18
57
|
name: string;
|
|
19
58
|
type: PluginType;
|
|
@@ -23,4 +62,6 @@ export declare const builder: BuilderHandle;
|
|
|
23
62
|
export declare function registerPlugin(plugin: Plugin): void;
|
|
24
63
|
export declare function getRegisteredThemes(): Theme[];
|
|
25
64
|
export declare function getAssetProvider(): AssetProvider | null;
|
|
65
|
+
export declare function getProductProvider(): ProductProvider | null;
|
|
66
|
+
export declare function getVoucherProvider(): VoucherProvider | null;
|
|
26
67
|
export declare function getAIProvider(): AIProvider | null;
|
package/dist/core/types.d.ts
CHANGED
|
@@ -1,4 +1,24 @@
|
|
|
1
1
|
export type ElementType = "text" | "image" | "button" | "spacer" | "divider" | "productGrid";
|
|
2
|
+
/**
|
|
3
|
+
* Marks a link as a well-known system link. Backend processors replace the
|
|
4
|
+
* placeholder href (e.g. `{{unsubscribe_url}}`) with a real per-recipient URL.
|
|
5
|
+
* The rendered HTML also carries a `data-link-type` attribute so backends that
|
|
6
|
+
* parse HTML can find and replace these links without inspecting the JSON.
|
|
7
|
+
*/
|
|
8
|
+
export type SpecialLinkType = "unsubscribe" | "view_in_browser" | "manage_preferences" | "user_profile";
|
|
9
|
+
/** Placeholder href values automatically set when a SpecialLinkType is chosen. */
|
|
10
|
+
export declare const SPECIAL_LINK_PLACEHOLDERS: Record<SpecialLinkType, string>;
|
|
11
|
+
/**
|
|
12
|
+
* A merge tag / personalisation token. Configured via the `mergeTags` prop on
|
|
13
|
+
* `<EmailBuilder>` or via `builder.registerMergeTags(tags)`.
|
|
14
|
+
* When the user picks a tag, `{attribute}` is inserted into the text content.
|
|
15
|
+
*/
|
|
16
|
+
export interface MergeTag {
|
|
17
|
+
/** Dot-notation path used as the placeholder, e.g. `"user.firstname"`. */
|
|
18
|
+
attribute: string;
|
|
19
|
+
/** Human-readable label shown in the sidebar dropdown, e.g. `"First name"`. */
|
|
20
|
+
title: string;
|
|
21
|
+
}
|
|
2
22
|
export interface BaseStyle {
|
|
3
23
|
paddingTop?: number;
|
|
4
24
|
paddingBottom?: number;
|
|
@@ -18,7 +38,7 @@ export interface BaseStyle {
|
|
|
18
38
|
export interface TextElement {
|
|
19
39
|
id: string;
|
|
20
40
|
type: "text";
|
|
21
|
-
role?: "headline" | "subheadline" | "body" | "caption";
|
|
41
|
+
role?: "headline" | "subheadline" | "body" | "caption" | "voucherCode";
|
|
22
42
|
content: string;
|
|
23
43
|
style?: BaseStyle & {
|
|
24
44
|
fontFamily?: string;
|
|
@@ -28,6 +48,8 @@ export interface TextElement {
|
|
|
28
48
|
fontWeight?: number | string;
|
|
29
49
|
color?: string;
|
|
30
50
|
link?: string;
|
|
51
|
+
/** Marks the element link as a well-known system link (e.g. unsubscribe). */
|
|
52
|
+
linkType?: SpecialLinkType;
|
|
31
53
|
};
|
|
32
54
|
}
|
|
33
55
|
export interface ImageElement {
|
|
@@ -36,6 +58,8 @@ export interface ImageElement {
|
|
|
36
58
|
src: string;
|
|
37
59
|
alt?: string;
|
|
38
60
|
link?: string;
|
|
61
|
+
/** Marks the image link as a well-known system link. */
|
|
62
|
+
linkType?: SpecialLinkType;
|
|
39
63
|
style?: BaseStyle & {
|
|
40
64
|
width?: number;
|
|
41
65
|
height?: number;
|
|
@@ -46,6 +70,8 @@ export interface ButtonElement {
|
|
|
46
70
|
type: "button";
|
|
47
71
|
label: string;
|
|
48
72
|
link: string;
|
|
73
|
+
/** Marks the button link as a well-known system link. */
|
|
74
|
+
linkType?: SpecialLinkType;
|
|
49
75
|
style?: BaseStyle & {
|
|
50
76
|
backgroundColor?: string;
|
|
51
77
|
color?: string;
|
|
@@ -79,6 +105,8 @@ export interface Product {
|
|
|
79
105
|
description?: string;
|
|
80
106
|
link?: string;
|
|
81
107
|
buttonLabel?: string;
|
|
108
|
+
/** Optional 0–5 rating. Rendered as star glyphs when the grid shows stars. */
|
|
109
|
+
stars?: number;
|
|
82
110
|
}
|
|
83
111
|
export interface ProductGridElement {
|
|
84
112
|
id: string;
|
|
@@ -88,6 +116,8 @@ export interface ProductGridElement {
|
|
|
88
116
|
showOldPrice: boolean;
|
|
89
117
|
showButton: boolean;
|
|
90
118
|
showDescription: boolean;
|
|
119
|
+
/** Show the star rating on each product card. Optional for back-compat. */
|
|
120
|
+
showStars?: boolean;
|
|
91
121
|
buttonLabel?: string;
|
|
92
122
|
style?: BaseStyle & {
|
|
93
123
|
nameColor?: string;
|
package/dist/core/utils.d.ts
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
1
|
export declare function uid(prefix?: string): string;
|
|
2
2
|
export declare function escapeHtml(s: string): string;
|
|
3
3
|
export declare function safeUrl(url: string | undefined): string;
|
|
4
|
+
/** Default gold used for product star ratings. */
|
|
5
|
+
export declare const STAR_COLOR = "#F5A623";
|
|
6
|
+
/**
|
|
7
|
+
* Build a 5-glyph star string for a 0–5 rating (rounded to the nearest whole
|
|
8
|
+
* star). Uses ★ (full) and ☆ (empty) so it renders in any email client.
|
|
9
|
+
*/
|
|
10
|
+
export declare function starGlyphs(rating: number): string;
|
|
@@ -16,12 +16,14 @@ export declare const moduleSchema: z.ZodObject<{
|
|
|
16
16
|
src: z.ZodString;
|
|
17
17
|
alt: z.ZodOptional<z.ZodString>;
|
|
18
18
|
link: z.ZodOptional<z.ZodString>;
|
|
19
|
+
linkType: z.ZodOptional<z.ZodString>;
|
|
19
20
|
style: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean, z.ZodRecord<z.ZodString, z.ZodAny>]>>>;
|
|
20
21
|
}, z.core.$strip>, z.ZodObject<{
|
|
21
22
|
id: z.ZodString;
|
|
22
23
|
type: z.ZodLiteral<"button">;
|
|
23
24
|
label: z.ZodString;
|
|
24
25
|
link: z.ZodString;
|
|
26
|
+
linkType: z.ZodOptional<z.ZodString>;
|
|
25
27
|
style: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean, z.ZodRecord<z.ZodString, z.ZodAny>]>>>;
|
|
26
28
|
}, z.core.$strip>, z.ZodObject<{
|
|
27
29
|
id: z.ZodString;
|
|
@@ -44,11 +46,13 @@ export declare const moduleSchema: z.ZodObject<{
|
|
|
44
46
|
description: z.ZodOptional<z.ZodString>;
|
|
45
47
|
link: z.ZodOptional<z.ZodString>;
|
|
46
48
|
buttonLabel: z.ZodOptional<z.ZodString>;
|
|
49
|
+
stars: z.ZodOptional<z.ZodNumber>;
|
|
47
50
|
}, z.core.$strip>>;
|
|
48
51
|
columns: z.ZodUnion<readonly [z.ZodLiteral<1>, z.ZodLiteral<2>, z.ZodLiteral<3>]>;
|
|
49
52
|
showOldPrice: z.ZodBoolean;
|
|
50
53
|
showButton: z.ZodBoolean;
|
|
51
54
|
showDescription: z.ZodBoolean;
|
|
55
|
+
showStars: z.ZodOptional<z.ZodBoolean>;
|
|
52
56
|
buttonLabel: z.ZodOptional<z.ZodString>;
|
|
53
57
|
style: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean, z.ZodRecord<z.ZodString, z.ZodAny>]>>>;
|
|
54
58
|
}, z.core.$strip>], "type">>;
|
|
@@ -102,12 +106,14 @@ export declare const documentSchema: z.ZodObject<{
|
|
|
102
106
|
src: z.ZodString;
|
|
103
107
|
alt: z.ZodOptional<z.ZodString>;
|
|
104
108
|
link: z.ZodOptional<z.ZodString>;
|
|
109
|
+
linkType: z.ZodOptional<z.ZodString>;
|
|
105
110
|
style: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean, z.ZodRecord<z.ZodString, z.ZodAny>]>>>;
|
|
106
111
|
}, z.core.$strip>, z.ZodObject<{
|
|
107
112
|
id: z.ZodString;
|
|
108
113
|
type: z.ZodLiteral<"button">;
|
|
109
114
|
label: z.ZodString;
|
|
110
115
|
link: z.ZodString;
|
|
116
|
+
linkType: z.ZodOptional<z.ZodString>;
|
|
111
117
|
style: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean, z.ZodRecord<z.ZodString, z.ZodAny>]>>>;
|
|
112
118
|
}, z.core.$strip>, z.ZodObject<{
|
|
113
119
|
id: z.ZodString;
|
|
@@ -130,11 +136,13 @@ export declare const documentSchema: z.ZodObject<{
|
|
|
130
136
|
description: z.ZodOptional<z.ZodString>;
|
|
131
137
|
link: z.ZodOptional<z.ZodString>;
|
|
132
138
|
buttonLabel: z.ZodOptional<z.ZodString>;
|
|
139
|
+
stars: z.ZodOptional<z.ZodNumber>;
|
|
133
140
|
}, z.core.$strip>>;
|
|
134
141
|
columns: z.ZodUnion<readonly [z.ZodLiteral<1>, z.ZodLiteral<2>, z.ZodLiteral<3>]>;
|
|
135
142
|
showOldPrice: z.ZodBoolean;
|
|
136
143
|
showButton: z.ZodBoolean;
|
|
137
144
|
showDescription: z.ZodBoolean;
|
|
145
|
+
showStars: z.ZodOptional<z.ZodBoolean>;
|
|
138
146
|
buttonLabel: z.ZodOptional<z.ZodString>;
|
|
139
147
|
style: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean, z.ZodRecord<z.ZodString, z.ZodAny>]>>>;
|
|
140
148
|
}, z.core.$strip>], "type">>;
|