@one-million-lines/email-builder 0.3.0 → 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 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
@@ -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 {
@@ -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;