@seliseblocks/mailcraft 0.1.1 → 0.2.1
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/DOCS.md +243 -16
- package/README.md +15 -6
- package/README.md.txt +135 -0
- package/dist/mailcraft-editor.bundle.js +87 -47
- package/dist/mailcraft-editor.bundle.js.map +3 -3
- package/examples/templates/activate-your-account.html +1 -1
- package/examples/templates/back-in-stock.html +1 -1
- package/examples/templates/cart-left-behind.html +1 -1
- package/examples/templates/community-giveaway.html +1 -1
- package/examples/templates/frontend-futures-invite.html +1 -1
- package/examples/templates/give-25-get-25.html +1 -1
- package/examples/templates/invoice-paid.html +1 -1
- package/examples/templates/meet-nova-launch.html +1 -1
- package/examples/templates/mega-weekend-sale.html +1 -1
- package/examples/templates/order-confirmed.html +80 -80
- package/examples/templates/rate-your-headphones.html +1 -1
- package/examples/templates/reset-your-password.html +1 -1
- package/examples/templates/thankyou-promo-code.html +1 -1
- package/examples/templates/the-sunday-brief.html +1 -1
- package/examples/templates/welcome-to-your-workspace.html +1 -1
- package/examples/templates/your-order-shipped.html +1 -1
- package/examples/templates/your-password-was-changed.html +1 -1
- package/examples/templates/your-signin-code.html +1 -1
- package/examples/vanilla.html +261 -47
- package/package.json +7 -2
- package/src/core/assets.js +10 -15
- package/src/core/binder.js +140 -118
- package/src/core/blocks.js +14 -1
- package/src/core/css-cascade.js +117 -117
- package/src/core/editor-core.js +1540 -1492
- package/src/core/export.js +158 -55
- package/src/core/i18n/ar.js +218 -177
- package/src/core/i18n/bg.js +195 -152
- package/src/core/i18n/bn.js +217 -176
- package/src/core/i18n/ca.js +195 -152
- package/src/core/i18n/cs.js +195 -152
- package/src/core/i18n/da.js +195 -152
- package/src/core/i18n/de-CH.js +195 -152
- package/src/core/i18n/de.js +195 -152
- package/src/core/i18n/dz.js +220 -179
- package/src/core/i18n/el.js +195 -152
- package/src/core/i18n/en.js +3 -11
- package/src/core/i18n/es.js +195 -152
- package/src/core/i18n/et.js +195 -152
- package/src/core/i18n/fi.js +195 -152
- package/src/core/i18n/fr.js +195 -152
- package/src/core/i18n/hr.js +195 -152
- package/src/core/i18n/hu.js +195 -152
- package/src/core/i18n/index.js +83 -83
- package/src/core/i18n/it.js +195 -152
- package/src/core/i18n/lt.js +195 -152
- package/src/core/i18n/lv.js +195 -152
- package/src/core/i18n/nb.js +195 -152
- package/src/core/i18n/nl.js +195 -152
- package/src/core/i18n/pl.js +195 -152
- package/src/core/i18n/pt.js +195 -152
- package/src/core/i18n/ro.js +195 -152
- package/src/core/i18n/ru.js +195 -152
- package/src/core/i18n/sk.js +195 -152
- package/src/core/i18n/sl.js +195 -152
- package/src/core/i18n/sv.js +195 -152
- package/src/core/i18n/tables.js +50 -50
- package/src/core/i18n/tr.js +195 -152
- package/src/core/i18n/uk.js +195 -152
- package/src/core/icons.js +237 -235
- package/src/core/ids.js +1 -1
- package/src/core/import-html.js +1047 -959
- package/src/core/layout-style.js +100 -100
- package/src/core/parse.js +10 -10
- package/src/core/placeholder.js +15 -15
- package/src/core/sanitize.js +141 -141
- package/src/core/storage-limits.js +189 -184
- package/src/core/storage.js +85 -85
- package/src/core/theme.js +15 -1
- package/src/core/variables.js +11 -11
- package/src/index.js +9 -9
- package/src/mailcraft-editor.js +64 -25
- package/src/render/block-body.js +49 -6
- package/src/render/canvas.js +74 -8
- package/src/render/fields.js +619 -588
- package/src/render/focus-preserve.js +158 -158
- package/src/render/rte.js +241 -212
- package/src/render/screenshot.js +138 -132
- package/src/render/story.js +415 -415
- package/src/render/style.js +44 -8
- package/types/index.d.ts +422 -0
package/src/render/style.js
CHANGED
|
@@ -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
|
|
@@ -192,7 +200,7 @@ export const STYLE = `
|
|
|
192
200
|
}
|
|
193
201
|
|
|
194
202
|
/* Dotted-grid workspace background, shared by the canvas and the preview body. */
|
|
195
|
-
.mc-workspace
|
|
203
|
+
.mc-workspace {
|
|
196
204
|
background-color: var(--ed-work);
|
|
197
205
|
background-image: radial-gradient(circle, var(--ed-grid) 1px, transparent 1.2px);
|
|
198
206
|
background-size: 22px 22px;
|
|
@@ -213,8 +221,12 @@ export const STYLE = `
|
|
|
213
221
|
.mc-sheet-wrap { padding: 0; border: 0; border-radius: 0; background: transparent; box-shadow: none; }
|
|
214
222
|
.mc-workspace [data-mc-sheet="1"] { border: 1px solid var(--ed-line); border-radius: 6px; box-shadow: 0 2px 5px rgba(15,23,42,0.05), 0 14px 38px rgba(15,23,42,0.10); }
|
|
215
223
|
#mc[data-chrome="dark"] .mc-workspace [data-mc-sheet="1"] { border-color: rgba(148,163,184,0.4); box-shadow: 0 0 0 1px rgba(15,23,42,0.4), 0 22px 58px rgba(0,0,0,0.36); }
|
|
216
|
-
|
|
217
|
-
|
|
224
|
+
/* The preview body is the *sent* email's page: the theme's own page
|
|
225
|
+
background paints it edge to edge (set inline by renderPreviewModal) and
|
|
226
|
+
the sheet sits flush -- no dotted grid, no padding, no framing. What you
|
|
227
|
+
see is what exportHtml() gives the recipient. */
|
|
228
|
+
.mc-preview-body [data-mc-sheet="1"] { border: 0; border-radius: 0; box-shadow: none; }
|
|
229
|
+
#mc[data-chrome="dark"] .mc-preview-body [data-mc-sheet="1"] { border: 0; box-shadow: none; }
|
|
218
230
|
|
|
219
231
|
/* Final component polish mirrored from the approved standalone editor. */
|
|
220
232
|
#mc { --ed-success: #20a779; --ed-danger: #e05766; }
|
|
@@ -268,8 +280,7 @@ export const STYLE = `
|
|
|
268
280
|
#mc .mc-canvas-stage { padding: 28px 40px 150px !important; }
|
|
269
281
|
#mc .mc-workspace [data-mc-sheet="1"] { border: 1px solid rgba(15,23,42,.10); border-radius: 4px; background: #ffffff; box-shadow: 0 2px 5px rgba(15,23,42,.05), 0 14px 38px rgba(15,23,42,.10) !important; }
|
|
270
282
|
#mc[data-chrome="dark"] .mc-workspace [data-mc-sheet="1"] { border-color: rgba(148,163,184,.46); box-shadow: 0 0 0 1px rgba(15,23,42,.48), 0 22px 58px rgba(0,0,0,.38) !important; }
|
|
271
|
-
#mc .mc-preview-body [data-mc-sheet="1"] { border:
|
|
272
|
-
#mc[data-chrome="dark"] .mc-preview-body [data-mc-sheet="1"] { border-color: rgba(203,213,225,.42); box-shadow: 0 0 0 1px rgba(15,23,42,.65), 0 24px 65px rgba(0,0,0,.40) !important; }
|
|
283
|
+
#mc[data-chrome="dark"] .mc-preview-body [data-mc-sheet="1"] { border: 0; box-shadow: none !important; }
|
|
273
284
|
#mc .mc-inspector { box-shadow: -10px 0 30px rgba(42,47,77,.04); color: var(--ed-panel-label); }
|
|
274
285
|
#mc .mc-tabbar { grid-auto-flow: column; grid-auto-columns: minmax(0, 1fr); padding: 7px 8px 0; gap: 3px; border-bottom: 0 !important; background: var(--ed-panel) !important; overflow: visible; position: relative; z-index: 12; }
|
|
275
286
|
#mc .mc-tabbar button { position: relative; min-width: 0; height: 39px; border-radius: 8px 8px 0 0 !important; padding: 0 !important; display: flex; align-items: center; justify-content: center; font-family: var(--ed-font) !important; }
|
|
@@ -390,12 +401,18 @@ export const STYLE = `
|
|
|
390
401
|
#mc .mc-code-split { gap: 12px; padding: 12px; background: var(--ed-work); }
|
|
391
402
|
#mc .mc-code-source, #mc .mc-code-preview { border: 1px solid var(--ed-line) !important; border-radius: 12px; overflow: hidden; background: var(--ed-panel) !important; }
|
|
392
403
|
#mc .mc-pane-label { padding: 8px 13px !important; background: var(--ed-panel-2) !important; font-size: 9.5px !important; letter-spacing: .025em !important; text-transform: none !important; }
|
|
393
|
-
|
|
394
|
-
|
|
404
|
+
/* The live preview is the email, not a card of it: at full width the frame
|
|
405
|
+
fills the pane and the document's own page background is the only surround.
|
|
406
|
+
Only the 390px phone width gets the mat and the card, where the inset frame
|
|
407
|
+
is what reads as a device. */
|
|
408
|
+
#mc .mc-code-preview-body { padding: 0 !important; background: var(--ed-work); }
|
|
409
|
+
#mc .mc-code-preview-body.is-device { padding: 14px !important; }
|
|
410
|
+
#mc .mc-code-frame { border: 0 !important; border-radius: 0; box-shadow: none !important; }
|
|
411
|
+
#mc .mc-code-preview-body.is-device .mc-code-frame { border-radius: 9px; box-shadow: 0 5px 20px rgba(15,23,42,.10) !important; }
|
|
395
412
|
#mc .mc-code-footer { padding: 8px 16px !important; background: var(--ed-panel); font-family: var(--ed-font) !important; font-size: 9.5px !important; letter-spacing: 0 !important; }
|
|
396
413
|
#mc .mc-preview-panel { grid-template-rows: 58px minmax(0,1fr) !important; }
|
|
397
414
|
#mc .mc-preview-title { font-family: var(--ed-font); font-size: 12px !important; font-weight: 600; }
|
|
398
|
-
#mc .mc-preview-body { padding:
|
|
415
|
+
#mc .mc-preview-body { padding: 0 !important; }
|
|
399
416
|
#mc .mc-toast { border-radius: 10px; box-shadow: 0 12px 34px rgba(16,19,36,.28) !important; }
|
|
400
417
|
#mc[data-chrome="dark"] .mc-shell { box-shadow: 0 24px 70px rgba(0,0,0,.44) !important; }
|
|
401
418
|
#mc[data-chrome="dark"] .mc-header { background: rgba(25,28,41,.88) !important; }
|
|
@@ -449,4 +466,23 @@ export const STYLE = `
|
|
|
449
466
|
/* The inspector inherits the active chrome palette. Keeping theme ownership
|
|
450
467
|
at #mc makes the header, menus, every panel and every field switch together
|
|
451
468
|
when light/dark changes, instead of leaving the side panel permanently dark. */
|
|
469
|
+
|
|
470
|
+
/*
|
|
471
|
+
* The page section (render/canvas.js) -- the full-width area the email sits
|
|
472
|
+
* on, painted from the document's own bg/padY/padX. It only takes the
|
|
473
|
+
* editor's frame once it is actually visible: at the 0 default it hugs the
|
|
474
|
+
* sheet exactly, so the sheet keeps the frame and an untouched canvas is
|
|
475
|
+
* pixel-identical to before. Once there is padding the page becomes the
|
|
476
|
+
* outermost surface and owns the border and lift, and the sheet drops to a
|
|
477
|
+
* small card shadow so its glow does not spill past the page's edge.
|
|
478
|
+
*/
|
|
479
|
+
#mc .mc-page.is-padded { border-radius: 6px; }
|
|
480
|
+
#mc .mc-workspace .mc-page.is-padded { border: 1px solid var(--ed-line); box-shadow: 0 2px 5px rgba(15,23,42,.05), 0 14px 38px rgba(15,23,42,.10); }
|
|
481
|
+
#mc .mc-workspace .mc-page.is-padded [data-mc-sheet="1"] { box-shadow: 0 1px 3px rgba(15,23,42,.12) !important; }
|
|
482
|
+
#mc[data-chrome="dark"] .mc-workspace .mc-page.is-padded { border-color: rgba(148,163,184,.46); box-shadow: 0 0 0 1px rgba(15,23,42,.48), 0 22px 58px rgba(0,0,0,.38); }
|
|
483
|
+
#mc[data-chrome="dark"] .mc-workspace .mc-page.is-padded [data-mc-sheet="1"] { box-shadow: 0 1px 4px rgba(0,0,0,.35) !important; }
|
|
484
|
+
#mc .mc-preview-body .mc-page.is-padded { border: 1px solid rgba(15,23,42,.12); border-radius: 10px; box-shadow: 0 18px 50px rgba(15,23,42,.13); }
|
|
485
|
+
#mc .mc-preview-body .mc-page.is-padded [data-mc-sheet="1"] { box-shadow: 0 1px 3px rgba(15,23,42,.12) !important; }
|
|
486
|
+
#mc[data-chrome="dark"] .mc-preview-body .mc-page.is-padded { border-color: rgba(203,213,225,.42); box-shadow: 0 0 0 1px rgba(15,23,42,.65), 0 24px 65px rgba(0,0,0,.40); }
|
|
487
|
+
#mc[data-chrome="dark"] .mc-preview-body .mc-page.is-padded [data-mc-sheet="1"] { box-shadow: 0 1px 4px rgba(0,0,0,.35) !important; }
|
|
452
488
|
`;
|
package/types/index.d.ts
ADDED
|
@@ -0,0 +1,422 @@
|
|
|
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
|
+
/**
|
|
78
|
+
* Allowed MIME types. Omitted or empty means every image type the validator
|
|
79
|
+
* recognizes (SVG still needs `allowSvg`).
|
|
80
|
+
*/
|
|
81
|
+
accept?: string[];
|
|
82
|
+
/** Per-file byte ceiling. Required. */
|
|
83
|
+
maxBytes: number;
|
|
84
|
+
maxWidth?: number;
|
|
85
|
+
maxHeight?: number;
|
|
86
|
+
maxFilesPerDrop?: number;
|
|
87
|
+
/** SVG is refused even when listed in `accept` unless this is also true. */
|
|
88
|
+
allowSvg?: boolean;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export interface StorageListQuery {
|
|
92
|
+
folderId: string;
|
|
93
|
+
/** Opaque — whatever the provider returned last, handed back to fetch the next page. */
|
|
94
|
+
cursor: string | null;
|
|
95
|
+
query: string;
|
|
96
|
+
/** Aborted when a newer listing supersedes this one. */
|
|
97
|
+
signal?: AbortSignal | null;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export interface StorageListResult {
|
|
101
|
+
items: Asset[];
|
|
102
|
+
/** `null` when there are no more pages. */
|
|
103
|
+
cursor: string | null;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export interface StorageUploadInfo {
|
|
107
|
+
folderId: string;
|
|
108
|
+
/** Pixel dimensions the validation pass already measured client-side. */
|
|
109
|
+
width: number;
|
|
110
|
+
height: number;
|
|
111
|
+
signal?: AbortSignal | null;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export interface StorageProvider {
|
|
115
|
+
/** One page of assets. */
|
|
116
|
+
list(q: StorageListQuery): Promise<StorageListResult>;
|
|
117
|
+
/** Stores one already-validated file and resolves to the asset that represents it. */
|
|
118
|
+
upload(file: File, info: StorageUploadInfo): Promise<Asset>;
|
|
119
|
+
/** Selectable folders. Omit for a flat library. */
|
|
120
|
+
folders?(): Promise<Array<{ id: string; name: string }>>;
|
|
121
|
+
/** Deletes. Without it the library's DEL only drops the tile from view. */
|
|
122
|
+
remove?(asset: Asset): Promise<void>;
|
|
123
|
+
/** Provider-declared ceilings. `editor.storageLimits` wins over these, per key. */
|
|
124
|
+
limits?: StorageLimits;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** The synthetic "all files" folder id (the empty string). */
|
|
128
|
+
export const ALL_FOLDER_ID: '';
|
|
129
|
+
|
|
130
|
+
/** Coerces whatever a provider returned into the library's asset shape; `probe` fills in what the client already measured. */
|
|
131
|
+
export function normalizeAsset(
|
|
132
|
+
raw: unknown,
|
|
133
|
+
probe?: { name?: string; w?: number; ht?: number; size?: number } | null,
|
|
134
|
+
): Asset;
|
|
135
|
+
|
|
136
|
+
/** `hostLimits` over `providerLimits`, per key. `null` when neither is set. */
|
|
137
|
+
export function resolveLimits(
|
|
138
|
+
hostLimits?: Partial<StorageLimits> | null,
|
|
139
|
+
providerLimits?: Partial<StorageLimits> | null,
|
|
140
|
+
): StorageLimits | null;
|
|
141
|
+
|
|
142
|
+
// ---------------------------------------------------------------------------
|
|
143
|
+
// Upload validation, reusable outside the editor
|
|
144
|
+
|
|
145
|
+
export interface AcceptedFile {
|
|
146
|
+
file: File;
|
|
147
|
+
/** Sanitized name — see `sanitizeName`. */
|
|
148
|
+
name: string;
|
|
149
|
+
/** Byte-sniffed MIME type, never `file.type`. */
|
|
150
|
+
type: string;
|
|
151
|
+
size: number;
|
|
152
|
+
w: number;
|
|
153
|
+
ht: number;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export interface RejectedFile {
|
|
157
|
+
name: string;
|
|
158
|
+
/** An i18n key (`storage.err*`) naming the reason, translated by the same table as the chrome. */
|
|
159
|
+
key: string;
|
|
160
|
+
params: Record<string, string | number>;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** Splits a file list into what may be uploaded and what may not, against the given limits. */
|
|
164
|
+
export function validateFiles(
|
|
165
|
+
list: ArrayLike<File> | Iterable<File> | null | undefined,
|
|
166
|
+
limits?: Partial<StorageLimits> | null,
|
|
167
|
+
): Promise<{ accepted: AcceptedFile[]; rejected: RejectedFile[] }>;
|
|
168
|
+
|
|
169
|
+
/** Strips path syntax and control characters from a filename and caps its length. */
|
|
170
|
+
export function sanitizeName(name: string): string;
|
|
171
|
+
|
|
172
|
+
/** An `accept` attribute for the file picker, so the OS dialog greys out what validation would refuse. */
|
|
173
|
+
export function acceptAttribute(limits?: Partial<StorageLimits> | null): string;
|
|
174
|
+
|
|
175
|
+
/** The i18n key naming what a limits object is missing, or `null` when it is usable. */
|
|
176
|
+
export function limitsProblem(limits: unknown): string | null;
|
|
177
|
+
|
|
178
|
+
// ---------------------------------------------------------------------------
|
|
179
|
+
// Editor options shared by the attribute/property surface and createEditor
|
|
180
|
+
|
|
181
|
+
export type ToolbarPart =
|
|
182
|
+
| 'logo' | 'status' | 'device' | 'undo' | 'redo'
|
|
183
|
+
| 'theme' | 'ai' | 'code' | 'preview' | 'export';
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* The `toolbar` property: `false` for no bar, or an object where only the
|
|
187
|
+
* keys set to `false` do anything — unlisted parts stay on. (The attribute
|
|
188
|
+
* form is a string: `none`, `all`, or a comma list of the parts to keep.)
|
|
189
|
+
*/
|
|
190
|
+
export type ToolbarOption = boolean | string | Partial<Record<ToolbarPart, boolean>>;
|
|
191
|
+
|
|
192
|
+
/** The `footer` property: `false` to remove the strip, a string to replace the line, or a config object. */
|
|
193
|
+
export type FooterOption =
|
|
194
|
+
| boolean
|
|
195
|
+
| string
|
|
196
|
+
| { text?: string; href?: string; target?: string; show?: boolean };
|
|
197
|
+
|
|
198
|
+
/** One `async (prompt) => text` function — the whole AI seam. */
|
|
199
|
+
export type AiProvider = (prompt: string) => string | Promise<string>;
|
|
200
|
+
|
|
201
|
+
/** Social-icon override. Falls back to the built-in icon when unset, throwing, or returning a non-node. */
|
|
202
|
+
export type IconProvider = (
|
|
203
|
+
platformKey: string,
|
|
204
|
+
ctx: { label: string; size: number; color: string },
|
|
205
|
+
) => Node;
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* The editor's internal document. NOT a public contract — the shape is free
|
|
209
|
+
* to change between versions. Store `exportHtml()` instead; this type exists
|
|
210
|
+
* so the `change` event and the internal accessors have something to name.
|
|
211
|
+
*/
|
|
212
|
+
export type EmailDocument = Record<string, any>;
|
|
213
|
+
|
|
214
|
+
/** What `loadTemplate` accepts: `html` (through the importer), a `doc`, or a `build()` that makes one per use. */
|
|
215
|
+
export interface Template {
|
|
216
|
+
name?: string;
|
|
217
|
+
html?: string;
|
|
218
|
+
doc?: EmailDocument;
|
|
219
|
+
build?: () => EmailDocument;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// ---------------------------------------------------------------------------
|
|
223
|
+
// The element
|
|
224
|
+
|
|
225
|
+
export interface MailCraftEditorEventMap extends HTMLElementEventMap {
|
|
226
|
+
/** The internal document — for dirty-tracking, not persistence. */
|
|
227
|
+
change: CustomEvent<EmailDocument>;
|
|
228
|
+
/** The exported HTML string. */
|
|
229
|
+
export: CustomEvent<string>;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* The `<mailcraft-editor>` custom element. Importing the package registers it.
|
|
234
|
+
*/
|
|
235
|
+
export class MailCraftEditor extends HTMLElement {
|
|
236
|
+
/** Merge variables. Reads back as an array; accepts a comma-separated string or an array. */
|
|
237
|
+
get variables(): string[];
|
|
238
|
+
set variables(value: string | string[]);
|
|
239
|
+
|
|
240
|
+
/** `false` for no bar, `{ part: false }` to drop parts. Reads back whatever was set (or the attribute string). */
|
|
241
|
+
get toolbar(): ToolbarOption;
|
|
242
|
+
set toolbar(value: ToolbarOption);
|
|
243
|
+
|
|
244
|
+
/** `false` to remove the strip, a string to replace it, or `{ text, href, target, show }`. */
|
|
245
|
+
get footer(): FooterOption;
|
|
246
|
+
set footer(value: FooterOption);
|
|
247
|
+
|
|
248
|
+
/** `inherit` or a CSS font-family stack. Editor chrome only. */
|
|
249
|
+
uiFont: string;
|
|
250
|
+
|
|
251
|
+
/** A CSS color, `var(--token)`, or `inherit`. One color repaints the editor chrome. */
|
|
252
|
+
accent: string;
|
|
253
|
+
|
|
254
|
+
/** Per-key overrides for the UI strings; `EN` lists every key. */
|
|
255
|
+
messages: MessageTable | null;
|
|
256
|
+
|
|
257
|
+
aiProvider: AiProvider | null;
|
|
258
|
+
iconProvider: IconProvider | null;
|
|
259
|
+
|
|
260
|
+
/** `null` drops back to the empty local library. */
|
|
261
|
+
storageProvider: StorageProvider | null;
|
|
262
|
+
|
|
263
|
+
/** Merged over `storageProvider.limits` per key, this side winning. */
|
|
264
|
+
storageLimits: StorageLimits | null;
|
|
265
|
+
|
|
266
|
+
/** Send-ready email HTML — valid input to the importer, so saving the export is saving the work. */
|
|
267
|
+
exportHtml(): string;
|
|
268
|
+
|
|
269
|
+
/** Parses email HTML back onto the canvas. Returns the number of rows produced. */
|
|
270
|
+
importHtml(html: string): number;
|
|
271
|
+
|
|
272
|
+
/** Applies a template as a normal undoable edit. The input is never mutated. */
|
|
273
|
+
loadTemplate(tpl: Template): void;
|
|
274
|
+
|
|
275
|
+
undo(): void;
|
|
276
|
+
redo(): void;
|
|
277
|
+
|
|
278
|
+
/** The full template as a PNG. */
|
|
279
|
+
screenshotPng(): Promise<Blob>;
|
|
280
|
+
|
|
281
|
+
/** Opens the story-style screenshot viewer. */
|
|
282
|
+
previewScreenshot(): void;
|
|
283
|
+
|
|
284
|
+
/** Saves a screenshot — captures first if no blob is passed. */
|
|
285
|
+
downloadScreenshot(blob?: Blob): Promise<void>;
|
|
286
|
+
|
|
287
|
+
/** Copies a screenshot to the clipboard — captures first if no blob is passed. */
|
|
288
|
+
copyScreenshot(blob?: Blob): Promise<void>;
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* NOT public API: the internal document, kept for undo, autosave and tests.
|
|
292
|
+
* Its shape is free to change between versions — store `exportHtml()` instead.
|
|
293
|
+
*/
|
|
294
|
+
getContent(): EmailDocument;
|
|
295
|
+
/** NOT public API — see `getContent`. */
|
|
296
|
+
setContent(doc: EmailDocument): void;
|
|
297
|
+
|
|
298
|
+
/** The internal engine. Its shape is free to change between versions. */
|
|
299
|
+
readonly core: EditorCore;
|
|
300
|
+
|
|
301
|
+
addEventListener<K extends keyof MailCraftEditorEventMap>(
|
|
302
|
+
type: K,
|
|
303
|
+
listener: (this: MailCraftEditor, ev: MailCraftEditorEventMap[K]) => any,
|
|
304
|
+
options?: boolean | AddEventListenerOptions,
|
|
305
|
+
): void;
|
|
306
|
+
addEventListener(
|
|
307
|
+
type: string,
|
|
308
|
+
listener: EventListenerOrEventListenerObject,
|
|
309
|
+
options?: boolean | AddEventListenerOptions,
|
|
310
|
+
): void;
|
|
311
|
+
removeEventListener<K extends keyof MailCraftEditorEventMap>(
|
|
312
|
+
type: K,
|
|
313
|
+
listener: (this: MailCraftEditor, ev: MailCraftEditorEventMap[K]) => any,
|
|
314
|
+
options?: boolean | EventListenerOptions,
|
|
315
|
+
): void;
|
|
316
|
+
removeEventListener(
|
|
317
|
+
type: string,
|
|
318
|
+
listener: EventListenerOrEventListenerObject,
|
|
319
|
+
options?: boolean | EventListenerOptions,
|
|
320
|
+
): void;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// ---------------------------------------------------------------------------
|
|
324
|
+
// createEditor — mount into a container
|
|
325
|
+
|
|
326
|
+
export interface CreateEditorOptions {
|
|
327
|
+
/** Initial email HTML, applied through the importer as an undoable edit. */
|
|
328
|
+
html?: string;
|
|
329
|
+
/** Template name for that content — only read alongside `html`. */
|
|
330
|
+
name?: string;
|
|
331
|
+
variables?: string | string[];
|
|
332
|
+
/** One of the 31 shipped tags. */
|
|
333
|
+
locale?: string;
|
|
334
|
+
/** Defaults from `locale`. */
|
|
335
|
+
dir?: 'ltr' | 'rtl';
|
|
336
|
+
theme?: 'light' | 'dark';
|
|
337
|
+
uiFont?: string;
|
|
338
|
+
accent?: string;
|
|
339
|
+
toolbar?: ToolbarOption;
|
|
340
|
+
footer?: FooterOption;
|
|
341
|
+
storageProvider?: StorageProvider | null;
|
|
342
|
+
storageLimits?: StorageLimits | null;
|
|
343
|
+
aiProvider?: AiProvider | null;
|
|
344
|
+
iconProvider?: IconProvider | null;
|
|
345
|
+
messages?: MessageTable | null;
|
|
346
|
+
/** Sets the container's height; a number is treated as px. Omit it and your CSS decides. */
|
|
347
|
+
height?: string | number;
|
|
348
|
+
/** Empty the container first (default: append). */
|
|
349
|
+
replace?: boolean;
|
|
350
|
+
onChange?(doc: EmailDocument): void;
|
|
351
|
+
onExport?(html: string): void;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
export interface EditorHandle {
|
|
355
|
+
/** The underlying custom element — everything the wrapper does not forward. */
|
|
356
|
+
element: MailCraftEditor;
|
|
357
|
+
/** Removes the editor and detaches the listeners this call attached. */
|
|
358
|
+
destroy(): void;
|
|
359
|
+
exportHtml(): string;
|
|
360
|
+
importHtml(html: string): number;
|
|
361
|
+
loadTemplate(tpl: Template): void;
|
|
362
|
+
undo(): void;
|
|
363
|
+
redo(): void;
|
|
364
|
+
screenshotPng(): Promise<Blob>;
|
|
365
|
+
previewScreenshot(): void;
|
|
366
|
+
downloadScreenshot(blob?: Blob): Promise<void>;
|
|
367
|
+
copyScreenshot(blob?: Blob): Promise<void>;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/** Creates a `<mailcraft-editor>` inside `target` (a CSS selector or element). Throws when nothing matches. */
|
|
371
|
+
export function createEditor(target: string | Element, options?: CreateEditorOptions): EditorHandle;
|
|
372
|
+
|
|
373
|
+
/** True once the custom element is registered — importing the package is what registers it. */
|
|
374
|
+
export function isReady(): boolean;
|
|
375
|
+
|
|
376
|
+
// ---------------------------------------------------------------------------
|
|
377
|
+
// Internals, exported for building your own UI on top.
|
|
378
|
+
// Typed loosely on purpose: their shapes are free to change between versions.
|
|
379
|
+
|
|
380
|
+
export class EditorCore {
|
|
381
|
+
constructor(options?: {
|
|
382
|
+
variables?: string | string[];
|
|
383
|
+
aiProvider?: AiProvider | null;
|
|
384
|
+
iconProvider?: IconProvider | null;
|
|
385
|
+
messages?: MessageTable | null;
|
|
386
|
+
storageProvider?: StorageProvider | null;
|
|
387
|
+
storageLimits?: StorageLimits | null;
|
|
388
|
+
});
|
|
389
|
+
state: Record<string, any>;
|
|
390
|
+
[key: string]: any;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/** Renders the document tree: `live = true` is the editable canvas, `false` the static preview/export tree. */
|
|
394
|
+
export function renderDoc(core: EditorCore, live?: boolean): HTMLElement;
|
|
395
|
+
|
|
396
|
+
export interface BlockDef {
|
|
397
|
+
type: string;
|
|
398
|
+
code: string;
|
|
399
|
+
label: string;
|
|
400
|
+
hint: string;
|
|
401
|
+
make(): Record<string, any>;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/** The 20 content-block definitions. */
|
|
405
|
+
export const BLOCKS: BlockDef[];
|
|
406
|
+
|
|
407
|
+
/** Compound presets — `build()` returns full row(s) built from ordinary blocks. */
|
|
408
|
+
export const GROUPS: Record<string, { label: string; icon: string; build(): Array<Record<string, any>> }>;
|
|
409
|
+
|
|
410
|
+
/** The section layouts (column span presets). */
|
|
411
|
+
export const LAYOUTS: Array<{ spans: number[]; label: string }>;
|
|
412
|
+
|
|
413
|
+
/** The palette order: `t` names a block type, `g` a group. */
|
|
414
|
+
export const PALETTE: Array<{ t?: string; g?: string }>;
|
|
415
|
+
|
|
416
|
+
// ---------------------------------------------------------------------------
|
|
417
|
+
|
|
418
|
+
declare global {
|
|
419
|
+
interface HTMLElementTagNameMap {
|
|
420
|
+
'mailcraft-editor': MailCraftEditor;
|
|
421
|
+
}
|
|
422
|
+
}
|