@kontakto/email-template-editor 1.6.0 → 2.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.
package/README.md CHANGED
@@ -49,14 +49,115 @@ function MyApp() {
49
49
  | `initialTemplate` | object | - | Initial template to load when editor first mounts |
50
50
  | `initialTemplateId` | string | - | ID of the initial template |
51
51
  | `initialTemplateName` | string | - | Name of the initial template |
52
- | `onSave` | function | - | Callback when template is saved: `(template) => void` |
52
+ | `onSave` | function | - | Callback when template is saved: `(payload: SavePayload) => void \| Promise<void>` (see SavePayload below) |
53
53
  | `onChange` | function | - | Callback when template changes: `(template) => void` |
54
- | `loadSamples` | function | - | Loads sample templates: `() => Promise<SampleTemplate[]>` |
55
- | `loadTemplates` | function | - | Loads user templates: `() => Promise<SampleTemplate[]>` |
54
+ | `loadSamples` | function | - | Loads sample templates: `() => Promise<TemplateListItem[]>` |
55
+ | `loadTemplates` | function | - | Loads user templates: `() => Promise<TemplateListItem[]>` |
56
56
  | `loadTemplate` | function | - | Loads specific template: `(id) => Promise<Template>` |
57
57
  | `deleteTemplate` | function | - | Deletes a template: `(id) => void` |
58
58
  | `copyTemplate` | function | - | Copies a template: `(name, content) => void` |
59
- | `saveAs` | function | - | Saves template with new name: `(name, content) => Promise<{id, name}>` |
59
+ | `renameTemplate` | function | - | Renames a template: `(id, newSlug) => void \| Promise<void>` |
60
+ | `setTemplateKind` | function | - | Promotes/demotes a row between template and sample: `(id, kind) => void \| Promise<void>`. When omitted, promote/demote menu items are hidden. |
61
+ | `saveAs` | function | - | Saves template with a new name: `(name, payload: SavePayload) => Promise<{ id, slug }>` |
62
+ | `uploadImage` | function | - | Uploads a single image file: `(file: File) => Promise<UploadedImage>`. Enables the Upload button on the Image inspector, drag-and-drop on the canvas, and paste-image-to-insert. When omitted, all upload UI is hidden and URL paste remains the only way to set an image. |
63
+ | `loadImages` | function | - | Lists previously uploaded images for the library picker: `() => Promise<LibraryImage[]>`. Enables the "Library" button on the Image inspector. When omitted, the library button is hidden. |
64
+ | `deleteImage` | function | - | Deletes an image from the library by URL: `(url: string) => Promise<void>`. When omitted, the per-row delete button in the library is hidden. |
65
+
66
+ `TemplateListItem` is the lean list-endpoint shape (no `editor_config`):
67
+
68
+ ```ts
69
+ type TemplateKind = 'template' | 'sample';
70
+
71
+ type TemplateListItem = {
72
+ id: string;
73
+ slug: string; // primary label
74
+ kind: TemplateKind; // 'template' (editable) or 'sample' (read-only starting point)
75
+ description?: string; // secondary line
76
+ subject?: string;
77
+ variables?: Array<{ name: string; description?: string }>;
78
+ tags?: string[];
79
+ thumbnailUrl?: string;
80
+ createdAt?: string;
81
+ updatedAt?: string;
82
+ };
83
+ ```
84
+
85
+ The drawer groups rows by `kind`, not by which callback returned them. Both `loadTemplates` and `loadSamples` should return their items with the correct `kind`; backends typically scope the two endpoints differently (per-user vs. org-wide), but the `kind` field is what determines the section a row appears in.
86
+
87
+ Samples are read-only starting points: Save on a loaded sample is disabled — the user must use Save As, which creates a fresh row with `kind='template'`.
88
+
89
+ #### Subject and variables
90
+
91
+ Email subject and template variables are stored on the `EmailLayout` block's data and round-trip with the editor configuration:
92
+
93
+ ```ts
94
+ type EmailLayoutData = {
95
+ // ...style fields
96
+ subject?: string;
97
+ variables?: Array<{ name: string; description?: string; sampleValue?: string }>;
98
+ };
99
+ ```
100
+
101
+ The editor renders a subject input above the canvas (always visible, supports `{{variable}}` syntax) and a Variables tab in the right inspector panel for declaring variables. Both persist via the standard save flow — consumers who previously stored `subject` in a separate DB column can read it from the saved `editor_config` instead.
102
+
103
+ The Variables tab supports Handlebars-aware management:
104
+
105
+ - **Add/rename/delete**. Names follow Handlebars identifier rules (`[A-Za-z_][A-Za-z0-9_]*`, max 64 chars, reserved words rejected). Renaming a declared variable rewrites all `{{oldName}}` and `{{oldName.*}}` tokens in the subject and in text/heading/button/html blocks — including inside block helpers (`{{#if}}`, `{{#each}}`, `{{#unless}}`, `{{#with}}`).
106
+ - **Usage indicators.** Each row shows how many times the variable is referenced, or "Unused in body" if the declared name never appears. Tokens found in the body that aren't declared surface at the top of the panel with a one-click "add as variable" action.
107
+ - **Insert at cursor.** Focus a text/heading/button/html editor or the subject input, then click the Insert button next to a variable to splice `{{name}}` at the caret.
108
+ - **Sample values.** Each row has an optional `sampleValue` field that travels with the template (persisted on `editor_config.root.data.variables[].sampleValue`). In Preview mode, `{{name}}` and `{{name.*}}` tokens in the subject and in text/heading/button/html blocks render with the sample value substituted in; block helpers (`{{#if}}`, `{{#each}}`, …) are stripped so their content renders inline, but the control flow is not evaluated. Edit mode always shows the raw tokens.
109
+
110
+ #### Save payload
111
+
112
+ `onSave` and `saveAs` receive the same `SavePayload`. The editor renders body HTML and plain text on every save so consumers don't ship the renderer themselves:
113
+
114
+ ```ts
115
+ type SavePayload = {
116
+ editorConfig: TEditorConfiguration; // source of truth
117
+ subject?: string; // from the subject input
118
+ variables?: Array<{ name: string; description?: string }>;
119
+ bodyHtml: string; // pre-rendered, ready to send
120
+ bodyText: string; // pre-rendered, ready to send
121
+ };
122
+ ```
123
+
124
+ The `renderToStaticMarkup` and `renderToText` utilities are also exposed publicly for consumers that need to re-render outside the save flow (e.g. batch jobs).
125
+
126
+ #### Image upload and library (BYO backend)
127
+
128
+ The editor delegates image storage to the consumer through three optional callbacks. When omitted, the corresponding UI is hidden and URL paste remains the fallback.
129
+
130
+ ```ts
131
+ type UploadedImage = {
132
+ url: string;
133
+ width?: number;
134
+ height?: number;
135
+ alt?: string;
136
+ };
137
+
138
+ type LibraryImage = UploadedImage & {
139
+ thumbnailUrl?: string;
140
+ uploadedAt?: string;
141
+ };
142
+ ```
143
+
144
+ - **`uploadImage(file)`** — receives a single `File`, uploads it (S3, R2, Bunny, presigned PUT, etc.), and returns the public `url` plus optional intrinsic dimensions and alt text. Wires up: the Upload button in the Image inspector, drag-and-drop of an image file onto the canvas, and paste-image-from-clipboard.
145
+ - **`loadImages()`** — returns the consumer's image list for the "Library" picker dialog (grid + filter by alt/URL).
146
+ - **`deleteImage(url)`** — removes an image from the library; surfaces a delete button on hover in the picker.
147
+
148
+ Reference upload handler:
149
+
150
+ ```ts
151
+ const uploadImage = async (file: File) => {
152
+ const form = new FormData();
153
+ form.append('file', file);
154
+ const res = await fetch('/api/images', { method: 'POST', body: form });
155
+ return res.json(); // { url, width, height }
156
+ };
157
+ ```
158
+
159
+ Newly uploaded images get their `width` / `height` set on the resulting Image block — important for Outlook, which needs explicit dimensions to lay the email out before images load.
160
+
60
161
  | `theme` | object | theme.ts | Custom theme for the EmailEditor, must be a Material UI theme object |
61
162
 
62
163
  #### Imperative API