@one-million-lines/email-builder 0.3.0 → 0.5.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 +90 -0
- package/dist/core/plugins.d.ts +3 -1
- package/dist/core/types.d.ts +26 -0
- package/dist/core/validation.d.ts +4 -0
- package/dist/editor/RichTextToolbar.d.ts +1 -0
- package/dist/editor/richTextState.d.ts +7 -0
- package/dist/email-builder.cjs +5 -6
- package/dist/email-builder.cjs.map +1 -1
- package/dist/email-builder.js +2366 -1849
- package/dist/email-builder.js.map +1 -1
- package/dist/index.d.ts +16 -2
- package/dist/modules/helpers.d.ts +28 -1
- package/dist/plugins/mergeTags/state.d.ts +8 -0
- package/dist/styles.css +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,96 @@ 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.5.0] — 2026-09-03
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- **Inline rich-text toolbar** — a floating, selection-aware formatting bar
|
|
12
|
+
appears whenever the cursor is inside a text element in the canvas.
|
|
13
|
+
- **Formatting**: Bold, Italic, Underline — toggle buttons that reflect the
|
|
14
|
+
current selection state via `queryCommandState`.
|
|
15
|
+
- **Font family** — email-safe font stacks: Arial, Georgia, Tahoma, Trebuchet
|
|
16
|
+
MS, Verdana, Courier New, Times New Roman. Wraps the selection in
|
|
17
|
+
`<span style="font-family:...">`.
|
|
18
|
+
- **Font size** — 10–48 px presets. Wraps the selection in
|
|
19
|
+
`<span style="font-size:...px">`.
|
|
20
|
+
- **Text colour** — 21 preset swatches plus a custom hex/name input. Uses
|
|
21
|
+
`styleWithCSS` + `execCommand('foreColor')` to produce
|
|
22
|
+
`<span style="color:...">`.
|
|
23
|
+
- **Insert link** — inline popover with a URL input and a special-link role
|
|
24
|
+
dropdown (Unsubscribe, View in browser, Manage preferences, User profile).
|
|
25
|
+
Wraps the selection in `<a href="..." data-link-type="...">` or inserts a
|
|
26
|
+
standalone linked anchor when nothing is selected.
|
|
27
|
+
- **Emoji picker** — 44 curated emojis in a compact grid; inserts at cursor.
|
|
28
|
+
- **Merge-tag inserter** — when `mergeTags` are configured, a tag dropdown
|
|
29
|
+
appears in the toolbar; selecting a tag inserts `{attribute}` at the cursor.
|
|
30
|
+
- The toolbar uses `onMouseDown + preventDefault` on all buttons so the
|
|
31
|
+
contenteditable never loses focus during formatting.
|
|
32
|
+
- New files: `src/editor/RichTextToolbar.tsx`, `src/editor/richTextState.ts`.
|
|
33
|
+
|
|
34
|
+
### Changed
|
|
35
|
+
|
|
36
|
+
- **Right sidebar — Text element panel** simplified. Removed per-selection
|
|
37
|
+
properties (font family, font size, font weight, colour, link URL, link role,
|
|
38
|
+
merge-tag inserter) which are now handled by the inline toolbar. The sidebar
|
|
39
|
+
now only shows block-level settings: default alignment, line height, letter
|
|
40
|
+
spacing, padding, and visibility (hide on mobile / desktop).
|
|
41
|
+
- `TextRender` in `Canvas.tsx` now registers/unregisters itself with
|
|
42
|
+
`useRichTextStore` on focus/blur, supplies a `data-email-text` attribute for
|
|
43
|
+
toolbar targeting, and prevents click events from bubbling when the element is
|
|
44
|
+
actively being edited (so caret repositioning clicks don't deselect the element).
|
|
45
|
+
|
|
46
|
+
## [0.4.0] — 2026-09-03
|
|
47
|
+
|
|
48
|
+
### Added
|
|
49
|
+
|
|
50
|
+
- **Special link types** — links can now be tagged with a semantic `linkType`
|
|
51
|
+
(`"unsubscribe"`, `"view_in_browser"`, `"manage_preferences"`, `"user_profile"`).
|
|
52
|
+
The rendered HTML carries both a `data-link-type` attribute on the anchor element
|
|
53
|
+
**and** a placeholder href value (e.g. `{{unsubscribe_url}}`), giving backend
|
|
54
|
+
processors two independent ways to locate and replace per-recipient URLs.
|
|
55
|
+
- New `SpecialLinkType` union type and `SPECIAL_LINK_PLACEHOLDERS` map exported
|
|
56
|
+
from `core/types`.
|
|
57
|
+
- `linkType` field added to `ButtonElement` and `ImageElement` (top-level), and
|
|
58
|
+
to `TextElement.style` (alongside the existing `link`).
|
|
59
|
+
- `text()`, `button()`, and `muted()` helpers accept a `linkType` option.
|
|
60
|
+
- New `footerLinks(links, opts?)` helper creates a text element with multiple
|
|
61
|
+
inline `<a>` tags, each carrying `data-link-type` — ideal for footer lines
|
|
62
|
+
that combine "Unsubscribe · View in browser".
|
|
63
|
+
- All built-in footer modules updated to use `footerLinks()` and proper
|
|
64
|
+
`linkType` values instead of plain text.
|
|
65
|
+
- Right sidebar: **Link role** dropdown added below the Link URL field for
|
|
66
|
+
Text, Image, and Button elements. Selecting a role auto-fills the placeholder
|
|
67
|
+
URL.
|
|
68
|
+
- `safeUrl()` updated to pass through `{{...}}` placeholder URLs without
|
|
69
|
+
stripping them.
|
|
70
|
+
- Validation schema updated to accept `linkType` on image and button elements.
|
|
71
|
+
|
|
72
|
+
- **Merge tags / personalisation tokens** — dynamic content placeholders can
|
|
73
|
+
now be configured and inserted directly from the editor sidebar.
|
|
74
|
+
- New `MergeTag` interface: `{ attribute: string; title: string }`.
|
|
75
|
+
- New `mergeTags` prop on `<EmailBuilder>` (React) and `createEmailBuilder`
|
|
76
|
+
(vanilla) accepts an array of merge tag definitions.
|
|
77
|
+
- New `builder.registerMergeTags(tags)` method on `BuilderHandle` for plugin
|
|
78
|
+
authors.
|
|
79
|
+
- New `useMergeTagsStore` reactive store (`src/plugins/mergeTags/state.ts`).
|
|
80
|
+
- Right sidebar: **Insert merge tag** dropdown appears in the Text element
|
|
81
|
+
panel when merge tags are configured. Selecting one appends
|
|
82
|
+
`{attribute}` to the text content.
|
|
83
|
+
- `MergeTag` exported from the public API.
|
|
84
|
+
|
|
85
|
+
- **Template & block authoring guide** (`src/templates/TEMPLATE_GUIDE.md`) —
|
|
86
|
+
comprehensive, AI-agent-ready reference documenting:
|
|
87
|
+
- The full `EmailDocument` JSON schema
|
|
88
|
+
- All element types and their style options
|
|
89
|
+
- Module structure, categories, and naming conventions
|
|
90
|
+
- Theme tokens and how to reference them
|
|
91
|
+
- All helper functions with options tables
|
|
92
|
+
- Special link types and merge tags
|
|
93
|
+
- Step-by-step instructions for creating new templates and module definitions
|
|
94
|
+
- A complete template code example
|
|
95
|
+
- A commit checklist and AI agent prompt template
|
|
96
|
+
|
|
7
97
|
## [Unreleased]
|
|
8
98
|
|
|
9
99
|
### Added
|
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<{
|
|
@@ -49,6 +49,8 @@ export interface BuilderHandle {
|
|
|
49
49
|
registerProductProvider: (provider: ProductProvider) => void;
|
|
50
50
|
registerVoucherProvider: (provider: VoucherProvider) => void;
|
|
51
51
|
setAIProvider: (provider: AIProvider) => void;
|
|
52
|
+
/** Configure the list of merge tags available in the text element sidebar. */
|
|
53
|
+
registerMergeTags: (tags: MergeTag[]) => void;
|
|
52
54
|
}
|
|
53
55
|
export type PluginType = "modules" | "themes" | "asset-provider" | "product-provider" | "voucher-provider" | "ai-provider";
|
|
54
56
|
export interface Plugin {
|
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;
|
|
@@ -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;
|
|
@@ -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;
|
|
@@ -104,12 +106,14 @@ export declare const documentSchema: z.ZodObject<{
|
|
|
104
106
|
src: z.ZodString;
|
|
105
107
|
alt: z.ZodOptional<z.ZodString>;
|
|
106
108
|
link: z.ZodOptional<z.ZodString>;
|
|
109
|
+
linkType: z.ZodOptional<z.ZodString>;
|
|
107
110
|
style: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean, z.ZodRecord<z.ZodString, z.ZodAny>]>>>;
|
|
108
111
|
}, z.core.$strip>, z.ZodObject<{
|
|
109
112
|
id: z.ZodString;
|
|
110
113
|
type: z.ZodLiteral<"button">;
|
|
111
114
|
label: z.ZodString;
|
|
112
115
|
link: z.ZodString;
|
|
116
|
+
linkType: z.ZodOptional<z.ZodString>;
|
|
113
117
|
style: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean, z.ZodRecord<z.ZodString, z.ZodAny>]>>>;
|
|
114
118
|
}, z.core.$strip>, z.ZodObject<{
|
|
115
119
|
id: z.ZodString;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function RichTextToolbar(): import("react").JSX.Element | null;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
interface RichTextState {
|
|
2
|
+
/** The currently focused contenteditable element, or null. */
|
|
3
|
+
activeEl: HTMLElement | null;
|
|
4
|
+
setActiveEl: (el: HTMLElement | null) => void;
|
|
5
|
+
}
|
|
6
|
+
export declare const useRichTextStore: import('zustand').UseBoundStore<import('zustand').StoreApi<RichTextState>>;
|
|
7
|
+
export {};
|