@seliseblocks/mailcraft 0.1.0 → 0.2.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.
Files changed (69) hide show
  1. package/DOCS.md +240 -12
  2. package/README.md +11 -3
  3. package/README.md.txt +134 -0
  4. package/dist/mailcraft-editor.bundle.js +51 -39
  5. package/dist/mailcraft-editor.bundle.js.map +3 -3
  6. package/examples/templates/order-confirmed.html +80 -80
  7. package/examples/vanilla.html +242 -23
  8. package/package.json +7 -2
  9. package/src/core/assets.js +10 -15
  10. package/src/core/binder.js +120 -118
  11. package/src/core/blocks.js +14 -1
  12. package/src/core/css-cascade.js +117 -117
  13. package/src/core/editor-core.js +1530 -1492
  14. package/src/core/export.js +142 -55
  15. package/src/core/i18n/ar.js +219 -177
  16. package/src/core/i18n/bg.js +196 -152
  17. package/src/core/i18n/bn.js +218 -176
  18. package/src/core/i18n/ca.js +196 -152
  19. package/src/core/i18n/cs.js +196 -152
  20. package/src/core/i18n/da.js +196 -152
  21. package/src/core/i18n/de-CH.js +196 -152
  22. package/src/core/i18n/de.js +196 -152
  23. package/src/core/i18n/dz.js +221 -179
  24. package/src/core/i18n/el.js +196 -152
  25. package/src/core/i18n/en.js +3 -10
  26. package/src/core/i18n/es.js +196 -152
  27. package/src/core/i18n/et.js +196 -152
  28. package/src/core/i18n/fi.js +196 -152
  29. package/src/core/i18n/fr.js +196 -152
  30. package/src/core/i18n/hr.js +196 -152
  31. package/src/core/i18n/hu.js +196 -152
  32. package/src/core/i18n/index.js +83 -83
  33. package/src/core/i18n/it.js +196 -152
  34. package/src/core/i18n/lt.js +196 -152
  35. package/src/core/i18n/lv.js +196 -152
  36. package/src/core/i18n/nb.js +196 -152
  37. package/src/core/i18n/nl.js +196 -152
  38. package/src/core/i18n/pl.js +196 -152
  39. package/src/core/i18n/pt.js +196 -152
  40. package/src/core/i18n/ro.js +196 -152
  41. package/src/core/i18n/ru.js +196 -152
  42. package/src/core/i18n/sk.js +196 -152
  43. package/src/core/i18n/sl.js +196 -152
  44. package/src/core/i18n/sv.js +196 -152
  45. package/src/core/i18n/tables.js +50 -50
  46. package/src/core/i18n/tr.js +196 -152
  47. package/src/core/i18n/uk.js +196 -152
  48. package/src/core/icons.js +237 -235
  49. package/src/core/ids.js +1 -1
  50. package/src/core/import-html.js +1025 -959
  51. package/src/core/layout-style.js +100 -100
  52. package/src/core/parse.js +10 -10
  53. package/src/core/placeholder.js +15 -15
  54. package/src/core/sanitize.js +141 -141
  55. package/src/core/storage-limits.js +184 -184
  56. package/src/core/storage.js +85 -85
  57. package/src/core/theme.js +1 -1
  58. package/src/core/variables.js +11 -11
  59. package/src/index.js +9 -9
  60. package/src/mailcraft-editor.js +26 -14
  61. package/src/render/block-body.js +49 -6
  62. package/src/render/canvas.js +31 -2
  63. package/src/render/fields.js +602 -588
  64. package/src/render/focus-preserve.js +158 -158
  65. package/src/render/rte.js +241 -212
  66. package/src/render/screenshot.js +132 -132
  67. package/src/render/story.js +415 -415
  68. package/src/render/style.js +8 -0
  69. package/types/index.d.ts +419 -0
@@ -109,6 +109,14 @@ export const STYLE = `
109
109
  .mc-block-el { outline: 1px solid transparent; outline-offset: 1px; transition: outline-color 0.12s; }
110
110
  .mc-block-el.is-selected { outline: 2px solid var(--ed-accent-sheet); }
111
111
  .mc-block-el:not(.is-selected):hover { outline: 1px dashed var(--ed-accent-sheet); }
112
+ /*
113
+ * The other half of the WebKit fix in render/canvas.js: a block is a
114
+ * draggable element, and Safari would rather drag the text inside it than
115
+ * let the caret select it. Taking the editable copy out of the drag path,
116
+ * and saying outright that it is selectable, keeps double-click-to-select
117
+ * and press-and-sweep working there. Harmless everywhere else.
118
+ */
119
+ [contenteditable="true"] { -webkit-user-drag: none; -webkit-user-select: text; user-select: text; }
112
120
  /*
113
121
  * The grab strip runs the row's whole left edge (see render/canvas.js); the
114
122
  * visible part is a round badge straddling the selection border. It takes
@@ -0,0 +1,419 @@
1
+ /**
2
+ * Hand-written type declarations for `@seliseblocks/mailcraft`.
3
+ *
4
+ * The runtime is plain JavaScript; these types describe the public contract
5
+ * documented in DOCS.md. Anything DOCS.md calls internal (`EditorCore`,
6
+ * `renderDoc`, the document object) is typed loosely on purpose — its shape
7
+ * is free to change between versions.
8
+ */
9
+
10
+ // ---------------------------------------------------------------------------
11
+ // i18n
12
+
13
+ /** A message table: i18n keys to translated strings. `EN` holds every key. */
14
+ export type MessageTable = Record<string, string>;
15
+
16
+ export interface LocaleInfo {
17
+ tag: string;
18
+ name: string;
19
+ rtl?: boolean;
20
+ }
21
+
22
+ /** The 31 shipped locale tags. */
23
+ export type LocaleTag =
24
+ | 'en' | 'ar' | 'bg' | 'bn' | 'ca' | 'cs' | 'da' | 'de' | 'de-CH' | 'dz'
25
+ | 'el' | 'es' | 'et' | 'fi' | 'fr' | 'hr' | 'hu' | 'it' | 'lt' | 'lv'
26
+ | 'nb' | 'nl' | 'pl' | 'pt' | 'ro' | 'ru' | 'sk' | 'sl' | 'sv' | 'tr' | 'uk';
27
+
28
+ /** Every locale that ships — metadata only, no message tables. */
29
+ export const LOCALES: readonly LocaleInfo[];
30
+
31
+ /** Every shipped message table, keyed by locale tag. Importing this pulls all translations in. */
32
+ export const LOCALE_TABLES: Record<LocaleTag, MessageTable>;
33
+
34
+ /** The English table — the fallback for every key a custom table leaves out. */
35
+ export const EN: MessageTable;
36
+
37
+ /** Every message key the editor ever asks for. */
38
+ export const MESSAGE_KEYS: readonly string[];
39
+
40
+ /** Resolves a key against `overrides`, then `EN`, then the key itself; interpolates `{name}` params. */
41
+ export function createTranslator(
42
+ overrides?: MessageTable | null,
43
+ ): (key: string, params?: Record<string, string | number>) => string;
44
+
45
+ /** Merges a locale over a base — the documented way to build a `.messages` value. */
46
+ export function defineMessages(base: MessageTable, overrides?: MessageTable): MessageTable;
47
+
48
+ /** Keys in `base` (default: `EN`) that `locale` does not translate, sorted. */
49
+ export function missingKeys(locale: MessageTable, base?: MessageTable): string[];
50
+
51
+ /** Whether a locale tag is written right-to-left. Metadata — `dir` is what flips the layout. */
52
+ export function isRtl(tag: string): boolean;
53
+
54
+ // ---------------------------------------------------------------------------
55
+ // Storage: the file-library contract the host implements
56
+
57
+ export interface Asset {
58
+ /** Stable id. With a provider this is the backend's file id — `remove()` gets it back verbatim. */
59
+ id: string;
60
+ /** Display/file name. */
61
+ name: string;
62
+ /** Resolvable image URL. Must outlive the send: an email renders it long after the editor closed. */
63
+ url: string;
64
+ /** Folder display name. */
65
+ folder: string;
66
+ /** Backend folder id, when the provider has one. */
67
+ folderId?: string;
68
+ /** Pixel width (0 when unknown). */
69
+ w: number;
70
+ /** Pixel height (0 when unknown). */
71
+ ht: number;
72
+ /** Bytes. */
73
+ size: number;
74
+ }
75
+
76
+ export interface StorageLimits {
77
+ /** Allowed MIME types, e.g. `['image/jpeg', 'image/png', 'image/gif']`. Required. */
78
+ accept: string[];
79
+ /** Per-file byte ceiling. Required. */
80
+ maxBytes: number;
81
+ maxWidth?: number;
82
+ maxHeight?: number;
83
+ maxFilesPerDrop?: number;
84
+ /** SVG is refused even when listed in `accept` unless this is also true. */
85
+ allowSvg?: boolean;
86
+ }
87
+
88
+ export interface StorageListQuery {
89
+ folderId: string;
90
+ /** Opaque — whatever the provider returned last, handed back to fetch the next page. */
91
+ cursor: string | null;
92
+ query: string;
93
+ /** Aborted when a newer listing supersedes this one. */
94
+ signal?: AbortSignal | null;
95
+ }
96
+
97
+ export interface StorageListResult {
98
+ items: Asset[];
99
+ /** `null` when there are no more pages. */
100
+ cursor: string | null;
101
+ }
102
+
103
+ export interface StorageUploadInfo {
104
+ folderId: string;
105
+ /** Pixel dimensions the validation pass already measured client-side. */
106
+ width: number;
107
+ height: number;
108
+ signal?: AbortSignal | null;
109
+ }
110
+
111
+ export interface StorageProvider {
112
+ /** One page of assets. */
113
+ list(q: StorageListQuery): Promise<StorageListResult>;
114
+ /** Stores one already-validated file and resolves to the asset that represents it. */
115
+ upload(file: File, info: StorageUploadInfo): Promise<Asset>;
116
+ /** Selectable folders. Omit for a flat library. */
117
+ folders?(): Promise<Array<{ id: string; name: string }>>;
118
+ /** Deletes. Without it the library's DEL only drops the tile from view. */
119
+ remove?(asset: Asset): Promise<void>;
120
+ /** Provider-declared ceilings. `editor.storageLimits` wins over these, per key. */
121
+ limits?: StorageLimits;
122
+ }
123
+
124
+ /** The synthetic "all files" folder id (the empty string). */
125
+ export const ALL_FOLDER_ID: '';
126
+
127
+ /** Coerces whatever a provider returned into the library's asset shape; `probe` fills in what the client already measured. */
128
+ export function normalizeAsset(
129
+ raw: unknown,
130
+ probe?: { name?: string; w?: number; ht?: number; size?: number } | null,
131
+ ): Asset;
132
+
133
+ /** `hostLimits` over `providerLimits`, per key. `null` when neither is set. */
134
+ export function resolveLimits(
135
+ hostLimits?: Partial<StorageLimits> | null,
136
+ providerLimits?: Partial<StorageLimits> | null,
137
+ ): StorageLimits | null;
138
+
139
+ // ---------------------------------------------------------------------------
140
+ // Upload validation, reusable outside the editor
141
+
142
+ export interface AcceptedFile {
143
+ file: File;
144
+ /** Sanitized name — see `sanitizeName`. */
145
+ name: string;
146
+ /** Byte-sniffed MIME type, never `file.type`. */
147
+ type: string;
148
+ size: number;
149
+ w: number;
150
+ ht: number;
151
+ }
152
+
153
+ export interface RejectedFile {
154
+ name: string;
155
+ /** An i18n key (`storage.err*`) naming the reason, translated by the same table as the chrome. */
156
+ key: string;
157
+ params: Record<string, string | number>;
158
+ }
159
+
160
+ /** Splits a file list into what may be uploaded and what may not, against the given limits. */
161
+ export function validateFiles(
162
+ list: ArrayLike<File> | Iterable<File> | null | undefined,
163
+ limits?: Partial<StorageLimits> | null,
164
+ ): Promise<{ accepted: AcceptedFile[]; rejected: RejectedFile[] }>;
165
+
166
+ /** Strips path syntax and control characters from a filename and caps its length. */
167
+ export function sanitizeName(name: string): string;
168
+
169
+ /** An `accept` attribute for the file picker, so the OS dialog greys out what validation would refuse. */
170
+ export function acceptAttribute(limits?: Partial<StorageLimits> | null): string;
171
+
172
+ /** The i18n key naming what a limits object is missing, or `null` when it is usable. */
173
+ export function limitsProblem(limits: unknown): string | null;
174
+
175
+ // ---------------------------------------------------------------------------
176
+ // Editor options shared by the attribute/property surface and createEditor
177
+
178
+ export type ToolbarPart =
179
+ | 'logo' | 'status' | 'device' | 'undo' | 'redo'
180
+ | 'theme' | 'ai' | 'code' | 'preview' | 'export';
181
+
182
+ /**
183
+ * The `toolbar` property: `false` for no bar, or an object where only the
184
+ * keys set to `false` do anything — unlisted parts stay on. (The attribute
185
+ * form is a string: `none`, `all`, or a comma list of the parts to keep.)
186
+ */
187
+ export type ToolbarOption = boolean | string | Partial<Record<ToolbarPart, boolean>>;
188
+
189
+ /** The `footer` property: `false` to remove the strip, a string to replace the line, or a config object. */
190
+ export type FooterOption =
191
+ | boolean
192
+ | string
193
+ | { text?: string; href?: string; target?: string; show?: boolean };
194
+
195
+ /** One `async (prompt) => text` function — the whole AI seam. */
196
+ export type AiProvider = (prompt: string) => string | Promise<string>;
197
+
198
+ /** Social-icon override. Falls back to the built-in icon when unset, throwing, or returning a non-node. */
199
+ export type IconProvider = (
200
+ platformKey: string,
201
+ ctx: { label: string; size: number; color: string },
202
+ ) => Node;
203
+
204
+ /**
205
+ * The editor's internal document. NOT a public contract — the shape is free
206
+ * to change between versions. Store `exportHtml()` instead; this type exists
207
+ * so the `change` event and the internal accessors have something to name.
208
+ */
209
+ export type EmailDocument = Record<string, any>;
210
+
211
+ /** What `loadTemplate` accepts: `html` (through the importer), a `doc`, or a `build()` that makes one per use. */
212
+ export interface Template {
213
+ name?: string;
214
+ html?: string;
215
+ doc?: EmailDocument;
216
+ build?: () => EmailDocument;
217
+ }
218
+
219
+ // ---------------------------------------------------------------------------
220
+ // The element
221
+
222
+ export interface MailCraftEditorEventMap extends HTMLElementEventMap {
223
+ /** The internal document — for dirty-tracking, not persistence. */
224
+ change: CustomEvent<EmailDocument>;
225
+ /** The exported HTML string. */
226
+ export: CustomEvent<string>;
227
+ }
228
+
229
+ /**
230
+ * The `<mailcraft-editor>` custom element. Importing the package registers it.
231
+ */
232
+ export class MailCraftEditor extends HTMLElement {
233
+ /** Merge variables. Reads back as an array; accepts a comma-separated string or an array. */
234
+ get variables(): string[];
235
+ set variables(value: string | string[]);
236
+
237
+ /** `false` for no bar, `{ part: false }` to drop parts. Reads back whatever was set (or the attribute string). */
238
+ get toolbar(): ToolbarOption;
239
+ set toolbar(value: ToolbarOption);
240
+
241
+ /** `false` to remove the strip, a string to replace it, or `{ text, href, target, show }`. */
242
+ get footer(): FooterOption;
243
+ set footer(value: FooterOption);
244
+
245
+ /** `inherit` or a CSS font-family stack. Editor chrome only. */
246
+ uiFont: string;
247
+
248
+ /** A CSS color, `var(--token)`, or `inherit`. One color repaints the editor chrome. */
249
+ accent: string;
250
+
251
+ /** Per-key overrides for the UI strings; `EN` lists every key. */
252
+ messages: MessageTable | null;
253
+
254
+ aiProvider: AiProvider | null;
255
+ iconProvider: IconProvider | null;
256
+
257
+ /** `null` drops back to the empty local library. */
258
+ storageProvider: StorageProvider | null;
259
+
260
+ /** Merged over `storageProvider.limits` per key, this side winning. */
261
+ storageLimits: StorageLimits | null;
262
+
263
+ /** Send-ready email HTML — valid input to the importer, so saving the export is saving the work. */
264
+ exportHtml(): string;
265
+
266
+ /** Parses email HTML back onto the canvas. Returns the number of rows produced. */
267
+ importHtml(html: string): number;
268
+
269
+ /** Applies a template as a normal undoable edit. The input is never mutated. */
270
+ loadTemplate(tpl: Template): void;
271
+
272
+ undo(): void;
273
+ redo(): void;
274
+
275
+ /** The full template as a PNG. */
276
+ screenshotPng(): Promise<Blob>;
277
+
278
+ /** Opens the story-style screenshot viewer. */
279
+ previewScreenshot(): void;
280
+
281
+ /** Saves a screenshot — captures first if no blob is passed. */
282
+ downloadScreenshot(blob?: Blob): Promise<void>;
283
+
284
+ /** Copies a screenshot to the clipboard — captures first if no blob is passed. */
285
+ copyScreenshot(blob?: Blob): Promise<void>;
286
+
287
+ /**
288
+ * NOT public API: the internal document, kept for undo, autosave and tests.
289
+ * Its shape is free to change between versions — store `exportHtml()` instead.
290
+ */
291
+ getContent(): EmailDocument;
292
+ /** NOT public API — see `getContent`. */
293
+ setContent(doc: EmailDocument): void;
294
+
295
+ /** The internal engine. Its shape is free to change between versions. */
296
+ readonly core: EditorCore;
297
+
298
+ addEventListener<K extends keyof MailCraftEditorEventMap>(
299
+ type: K,
300
+ listener: (this: MailCraftEditor, ev: MailCraftEditorEventMap[K]) => any,
301
+ options?: boolean | AddEventListenerOptions,
302
+ ): void;
303
+ addEventListener(
304
+ type: string,
305
+ listener: EventListenerOrEventListenerObject,
306
+ options?: boolean | AddEventListenerOptions,
307
+ ): void;
308
+ removeEventListener<K extends keyof MailCraftEditorEventMap>(
309
+ type: K,
310
+ listener: (this: MailCraftEditor, ev: MailCraftEditorEventMap[K]) => any,
311
+ options?: boolean | EventListenerOptions,
312
+ ): void;
313
+ removeEventListener(
314
+ type: string,
315
+ listener: EventListenerOrEventListenerObject,
316
+ options?: boolean | EventListenerOptions,
317
+ ): void;
318
+ }
319
+
320
+ // ---------------------------------------------------------------------------
321
+ // createEditor — mount into a container
322
+
323
+ export interface CreateEditorOptions {
324
+ /** Initial email HTML, applied through the importer as an undoable edit. */
325
+ html?: string;
326
+ /** Template name for that content — only read alongside `html`. */
327
+ name?: string;
328
+ variables?: string | string[];
329
+ /** One of the 31 shipped tags. */
330
+ locale?: string;
331
+ /** Defaults from `locale`. */
332
+ dir?: 'ltr' | 'rtl';
333
+ theme?: 'light' | 'dark';
334
+ uiFont?: string;
335
+ accent?: string;
336
+ toolbar?: ToolbarOption;
337
+ footer?: FooterOption;
338
+ storageProvider?: StorageProvider | null;
339
+ storageLimits?: StorageLimits | null;
340
+ aiProvider?: AiProvider | null;
341
+ iconProvider?: IconProvider | null;
342
+ messages?: MessageTable | null;
343
+ /** Sets the container's height; a number is treated as px. Omit it and your CSS decides. */
344
+ height?: string | number;
345
+ /** Empty the container first (default: append). */
346
+ replace?: boolean;
347
+ onChange?(doc: EmailDocument): void;
348
+ onExport?(html: string): void;
349
+ }
350
+
351
+ export interface EditorHandle {
352
+ /** The underlying custom element — everything the wrapper does not forward. */
353
+ element: MailCraftEditor;
354
+ /** Removes the editor and detaches the listeners this call attached. */
355
+ destroy(): void;
356
+ exportHtml(): string;
357
+ importHtml(html: string): number;
358
+ loadTemplate(tpl: Template): void;
359
+ undo(): void;
360
+ redo(): void;
361
+ screenshotPng(): Promise<Blob>;
362
+ previewScreenshot(): void;
363
+ downloadScreenshot(blob?: Blob): Promise<void>;
364
+ copyScreenshot(blob?: Blob): Promise<void>;
365
+ }
366
+
367
+ /** Creates a `<mailcraft-editor>` inside `target` (a CSS selector or element). Throws when nothing matches. */
368
+ export function createEditor(target: string | Element, options?: CreateEditorOptions): EditorHandle;
369
+
370
+ /** True once the custom element is registered — importing the package is what registers it. */
371
+ export function isReady(): boolean;
372
+
373
+ // ---------------------------------------------------------------------------
374
+ // Internals, exported for building your own UI on top.
375
+ // Typed loosely on purpose: their shapes are free to change between versions.
376
+
377
+ export class EditorCore {
378
+ constructor(options?: {
379
+ variables?: string | string[];
380
+ aiProvider?: AiProvider | null;
381
+ iconProvider?: IconProvider | null;
382
+ messages?: MessageTable | null;
383
+ storageProvider?: StorageProvider | null;
384
+ storageLimits?: StorageLimits | null;
385
+ });
386
+ state: Record<string, any>;
387
+ [key: string]: any;
388
+ }
389
+
390
+ /** Renders the document tree: `live = true` is the editable canvas, `false` the static preview/export tree. */
391
+ export function renderDoc(core: EditorCore, live?: boolean): HTMLElement;
392
+
393
+ export interface BlockDef {
394
+ type: string;
395
+ code: string;
396
+ label: string;
397
+ hint: string;
398
+ make(): Record<string, any>;
399
+ }
400
+
401
+ /** The 20 content-block definitions. */
402
+ export const BLOCKS: BlockDef[];
403
+
404
+ /** Compound presets — `build()` returns full row(s) built from ordinary blocks. */
405
+ export const GROUPS: Record<string, { label: string; icon: string; build(): Array<Record<string, any>> }>;
406
+
407
+ /** The section layouts (column span presets). */
408
+ export const LAYOUTS: Array<{ spans: number[]; label: string }>;
409
+
410
+ /** The palette order: `t` names a block type, `g` a group. */
411
+ export const PALETTE: Array<{ t?: string; g?: string }>;
412
+
413
+ // ---------------------------------------------------------------------------
414
+
415
+ declare global {
416
+ interface HTMLElementTagNameMap {
417
+ 'mailcraft-editor': MailCraftEditor;
418
+ }
419
+ }