@marlinjai/email-editor-core 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.
@@ -0,0 +1,1029 @@
1
+ // src/schema/gradient.ts
2
+ function buildGradientCSS(gradient) {
3
+ if (gradient.stops.length === 0) return void 0;
4
+ const stopList = gradient.stops.map((s) => `${s.color} ${s.position}%`).join(", ");
5
+ if (gradient.type === "radial") {
6
+ return `radial-gradient(circle, ${stopList})`;
7
+ }
8
+ return `linear-gradient(${gradient.angle}deg, ${stopList})`;
9
+ }
10
+
11
+ // src/schema/types.ts
12
+ function isWrapper(item) {
13
+ return item.type === "wrapper";
14
+ }
15
+ function allSections(items) {
16
+ return items.flatMap((item) => isWrapper(item) ? item.sections : [item]);
17
+ }
18
+
19
+ // src/schema/validation.ts
20
+ import { z } from "zod";
21
+ var SpacingSchema = z.object({
22
+ top: z.string().optional(),
23
+ right: z.string().optional(),
24
+ bottom: z.string().optional(),
25
+ left: z.string().optional()
26
+ });
27
+ var GradientStopSchema = z.object({
28
+ color: z.string().min(1),
29
+ position: z.number().min(0).max(100)
30
+ });
31
+ var BackgroundGradientSchema = z.object({
32
+ type: z.enum(["linear", "radial"]),
33
+ angle: z.number().min(0).max(360),
34
+ stops: z.array(GradientStopSchema).min(1)
35
+ });
36
+ var ExtraAttributesSchema = z.record(z.string().regex(/^[a-z][a-z0-9-]*$/), z.string());
37
+ var MjmlHeadSchema = z.object({
38
+ attributes: z.string().optional(),
39
+ bodyAttributes: ExtraAttributesSchema.optional(),
40
+ headRaw: z.string().optional()
41
+ });
42
+ var CustomFontSchema = z.object({
43
+ name: z.string(),
44
+ href: z.string()
45
+ });
46
+ var TemplateMetadataSchema = z.object({
47
+ name: z.string().optional(),
48
+ subject: z.string().optional(),
49
+ previewText: z.string().optional(),
50
+ title: z.string().optional(),
51
+ // Epoch milliseconds (what the editor's store emits) or an ISO 8601 string.
52
+ createdAt: z.union([z.string(), z.number()]).optional(),
53
+ updatedAt: z.union([z.string(), z.number()]).optional(),
54
+ // Head component settings
55
+ fonts: z.array(CustomFontSchema).optional(),
56
+ breakpoint: z.string().optional(),
57
+ customCSS: z.string().optional(),
58
+ inlineCSS: z.string().optional(),
59
+ mjmlHead: MjmlHeadSchema.optional()
60
+ });
61
+ var TextBlockSchema = z.object({
62
+ id: z.string(),
63
+ type: z.literal("text"),
64
+ hidden: z.boolean().optional(),
65
+ extraAttributes: ExtraAttributesSchema.optional(),
66
+ content: z.string(),
67
+ align: z.enum(["left", "center", "right", "justify"]).optional(),
68
+ color: z.string().optional(),
69
+ fontSize: z.string().optional(),
70
+ fontFamily: z.string().optional(),
71
+ padding: SpacingSchema.optional(),
72
+ lineHeight: z.string().optional()
73
+ });
74
+ var ImageBlockSchema = z.object({
75
+ id: z.string(),
76
+ type: z.literal("image"),
77
+ hidden: z.boolean().optional(),
78
+ extraAttributes: ExtraAttributesSchema.optional(),
79
+ src: z.string().min(1),
80
+ // Allow any non-empty string, not just URLs
81
+ alt: z.string().optional(),
82
+ width: z.string().optional(),
83
+ height: z.string().optional(),
84
+ align: z.enum(["left", "center", "right"]).optional(),
85
+ href: z.string().optional(),
86
+ // Allow any string for link
87
+ padding: SpacingSchema.optional(),
88
+ borderRadius: z.string().optional()
89
+ // Rounded corners
90
+ });
91
+ var ButtonBlockSchema = z.object({
92
+ id: z.string(),
93
+ type: z.literal("button"),
94
+ hidden: z.boolean().optional(),
95
+ extraAttributes: ExtraAttributesSchema.optional(),
96
+ label: z.string().min(1),
97
+ href: z.string().min(1),
98
+ // Allow any non-empty string
99
+ align: z.enum(["left", "center", "right"]).optional(),
100
+ backgroundColor: z.string().optional(),
101
+ color: z.string().optional(),
102
+ borderRadius: z.string().optional(),
103
+ border: z.string().optional(),
104
+ // CSS border shorthand
105
+ padding: SpacingSchema.optional(),
106
+ innerPadding: z.string().optional()
107
+ });
108
+ var DividerBlockSchema = z.object({
109
+ id: z.string(),
110
+ type: z.literal("divider"),
111
+ hidden: z.boolean().optional(),
112
+ extraAttributes: ExtraAttributesSchema.optional(),
113
+ borderColor: z.string().optional(),
114
+ borderWidth: z.string().optional(),
115
+ borderStyle: z.enum(["solid", "dashed", "dotted"]).optional(),
116
+ width: z.string().optional(),
117
+ // Width of the divider line
118
+ padding: SpacingSchema.optional()
119
+ });
120
+ var SpacerBlockSchema = z.object({
121
+ id: z.string(),
122
+ type: z.literal("spacer"),
123
+ hidden: z.boolean().optional(),
124
+ extraAttributes: ExtraAttributesSchema.optional(),
125
+ height: z.string()
126
+ });
127
+ var HeaderBlockSchema = z.object({
128
+ id: z.string(),
129
+ type: z.literal("header"),
130
+ hidden: z.boolean().optional(),
131
+ extraAttributes: ExtraAttributesSchema.optional(),
132
+ locked: z.literal(true)
133
+ });
134
+ var FooterBlockSchema = z.object({
135
+ id: z.string(),
136
+ type: z.literal("footer"),
137
+ hidden: z.boolean().optional(),
138
+ extraAttributes: ExtraAttributesSchema.optional(),
139
+ locked: z.literal(true)
140
+ });
141
+ var SocialBlockSchema = z.object({
142
+ id: z.string(),
143
+ type: z.literal("social"),
144
+ hidden: z.boolean().optional(),
145
+ extraAttributes: ExtraAttributesSchema.optional(),
146
+ mode: z.enum(["horizontal", "vertical"]).optional(),
147
+ align: z.enum(["left", "center", "right"]).optional(),
148
+ iconSize: z.string().optional(),
149
+ iconPadding: z.string().optional(),
150
+ borderRadius: z.string().optional(),
151
+ // For round icons
152
+ links: z.array(
153
+ z.object({
154
+ platform: z.enum(["facebook", "twitter", "instagram", "linkedin", "youtube", "pinterest", "github"]),
155
+ url: z.string(),
156
+ color: z.string().optional()
157
+ // Per-icon color override (uses platform default if not set)
158
+ })
159
+ )
160
+ });
161
+ var HeroBlockSchema = z.object({
162
+ id: z.string(),
163
+ type: z.literal("hero"),
164
+ hidden: z.boolean().optional(),
165
+ extraAttributes: ExtraAttributesSchema.optional(),
166
+ backgroundImage: z.string().min(1),
167
+ backgroundHeight: z.string().optional(),
168
+ backgroundWidth: z.string().optional(),
169
+ backgroundColor: z.string().optional(),
170
+ verticalAlign: z.enum(["top", "middle", "bottom"]).optional(),
171
+ mode: z.enum(["fluid-height", "fixed-height"]).optional()
172
+ });
173
+ var AccordionBlockSchema = z.object({
174
+ id: z.string(),
175
+ type: z.literal("accordion"),
176
+ hidden: z.boolean().optional(),
177
+ extraAttributes: ExtraAttributesSchema.optional(),
178
+ items: z.array(
179
+ z.object({
180
+ title: z.string(),
181
+ content: z.string()
182
+ })
183
+ ),
184
+ iconPosition: z.enum(["left", "right"]).optional(),
185
+ borderColor: z.string().optional(),
186
+ fontFamily: z.string().optional()
187
+ });
188
+ var RawBlockSchema = z.object({
189
+ id: z.string(),
190
+ type: z.literal("raw"),
191
+ hidden: z.boolean().optional(),
192
+ extraAttributes: ExtraAttributesSchema.optional(),
193
+ html: z.string()
194
+ });
195
+ var NavbarBlockSchema = z.object({
196
+ id: z.string(),
197
+ type: z.literal("navbar"),
198
+ hidden: z.boolean().optional(),
199
+ extraAttributes: ExtraAttributesSchema.optional(),
200
+ links: z.array(
201
+ z.object({
202
+ label: z.string(),
203
+ href: z.string(),
204
+ color: z.string().optional()
205
+ })
206
+ ),
207
+ hamburger: z.boolean().optional(),
208
+ baseUrl: z.string().optional(),
209
+ align: z.enum(["left", "center", "right"]).optional(),
210
+ icoColor: z.string().optional(),
211
+ padding: SpacingSchema.optional()
212
+ });
213
+ var CarouselBlockSchema = z.object({
214
+ id: z.string(),
215
+ type: z.literal("carousel"),
216
+ hidden: z.boolean().optional(),
217
+ extraAttributes: ExtraAttributesSchema.optional(),
218
+ images: z.array(
219
+ z.object({
220
+ src: z.string(),
221
+ alt: z.string().optional(),
222
+ href: z.string().optional(),
223
+ thumbnailSrc: z.string().optional()
224
+ })
225
+ ),
226
+ thumbnails: z.enum(["visible", "hidden"]).optional(),
227
+ borderRadius: z.string().optional(),
228
+ iconWidth: z.string().optional(),
229
+ tbBorderRadius: z.string().optional(),
230
+ padding: SpacingSchema.optional()
231
+ });
232
+ var TableBlockSchema = z.object({
233
+ id: z.string(),
234
+ type: z.literal("table"),
235
+ hidden: z.boolean().optional(),
236
+ extraAttributes: ExtraAttributesSchema.optional(),
237
+ headers: z.array(z.string()),
238
+ rows: z.array(z.array(z.string())),
239
+ align: z.enum(["left", "center", "right"]).optional(),
240
+ color: z.string().optional(),
241
+ fontFamily: z.string().optional(),
242
+ fontSize: z.string().optional(),
243
+ cellpadding: z.string().optional(),
244
+ cellspacing: z.string().optional(),
245
+ border: z.string().optional(),
246
+ padding: SpacingSchema.optional()
247
+ });
248
+ var BlockSchema = z.discriminatedUnion("type", [
249
+ TextBlockSchema,
250
+ ImageBlockSchema,
251
+ ButtonBlockSchema,
252
+ DividerBlockSchema,
253
+ SpacerBlockSchema,
254
+ HeaderBlockSchema,
255
+ FooterBlockSchema,
256
+ SocialBlockSchema,
257
+ HeroBlockSchema,
258
+ AccordionBlockSchema,
259
+ RawBlockSchema,
260
+ NavbarBlockSchema,
261
+ CarouselBlockSchema,
262
+ TableBlockSchema
263
+ ]);
264
+ var ColumnSchema = z.object({
265
+ id: z.string(),
266
+ width: z.number().min(0).max(100).optional(),
267
+ backgroundColor: z.string().optional(),
268
+ backgroundGradient: BackgroundGradientSchema.optional(),
269
+ padding: SpacingSchema.optional(),
270
+ verticalAlign: z.enum(["top", "middle", "bottom"]).optional(),
271
+ // Vertical content alignment
272
+ hidden: z.boolean().optional(),
273
+ extraAttributes: ExtraAttributesSchema.optional(),
274
+ blocks: z.array(BlockSchema)
275
+ });
276
+ var sectionFields = {
277
+ id: z.string(),
278
+ type: z.literal("section"),
279
+ backgroundColor: z.string().optional(),
280
+ backgroundImage: z.string().optional(),
281
+ backgroundPosition: z.string().optional(),
282
+ backgroundRepeat: z.enum(["repeat", "no-repeat"]).optional(),
283
+ backgroundSize: z.string().optional(),
284
+ backgroundGradient: BackgroundGradientSchema.optional(),
285
+ padding: SpacingSchema.optional(),
286
+ noStack: z.boolean().optional(),
287
+ fullWidth: z.boolean().optional(),
288
+ hidden: z.boolean().optional(),
289
+ extraAttributes: ExtraAttributesSchema.optional(),
290
+ bodyRaw: z.boolean().optional(),
291
+ columns: z.array(ColumnSchema).min(1)
292
+ };
293
+ var SectionSchema = z.object({
294
+ ...sectionFields,
295
+ isWrapper: z.undefined({ invalid_type_error: "isWrapper is a schema 1.0 field; a 1.1 document holds a wrapper instead" }).optional()
296
+ });
297
+ var SectionSchemaV1_0 = z.object({
298
+ ...sectionFields,
299
+ isWrapper: z.boolean().optional()
300
+ });
301
+ var WrapperSchema = z.object({
302
+ id: z.string(),
303
+ type: z.literal("wrapper"),
304
+ hidden: z.boolean().optional(),
305
+ backgroundColor: z.string().optional(),
306
+ backgroundImage: z.string().optional(),
307
+ backgroundGradient: BackgroundGradientSchema.optional(),
308
+ backgroundPosition: z.string().optional(),
309
+ backgroundRepeat: z.enum(["repeat", "no-repeat"]).optional(),
310
+ backgroundSize: z.string().optional(),
311
+ border: z.string().optional(),
312
+ borderTop: z.string().optional(),
313
+ borderRight: z.string().optional(),
314
+ borderBottom: z.string().optional(),
315
+ borderLeft: z.string().optional(),
316
+ borderRadius: z.string().optional(),
317
+ padding: SpacingSchema.optional(),
318
+ fullWidth: z.boolean().optional(),
319
+ cssClass: z.string().optional(),
320
+ gap: z.string().regex(/^[0-9]+(\.[0-9]+)?px$/, "gap is a length in px, e.g. 16px").optional(),
321
+ textAlign: z.enum(["left", "center", "right"]).optional(),
322
+ extraAttributes: ExtraAttributesSchema.optional(),
323
+ sections: z.array(SectionSchema)
324
+ });
325
+ var TopLevelItemSchema = z.discriminatedUnion("type", [SectionSchema, WrapperSchema]);
326
+ var EmailTemplateSchemaV1_0 = z.object({
327
+ id: z.string().optional(),
328
+ version: z.literal("1.0"),
329
+ metadata: TemplateMetadataSchema,
330
+ sections: z.array(SectionSchemaV1_0)
331
+ });
332
+ var EmailTemplateSchemaV1_1 = z.object({
333
+ id: z.string().optional(),
334
+ version: z.literal("1.1"),
335
+ metadata: TemplateMetadataSchema,
336
+ sections: z.array(TopLevelItemSchema)
337
+ });
338
+ var EmailTemplateSchema = z.discriminatedUnion("version", [EmailTemplateSchemaV1_0, EmailTemplateSchemaV1_1]);
339
+ function validateTemplate(template) {
340
+ return EmailTemplateSchema.safeParse(template);
341
+ }
342
+
343
+ // src/schema/migrate.ts
344
+ import { nanoid } from "nanoid";
345
+ var CURRENT_TEMPLATE_VERSION = "1.1";
346
+ var SUPPORTED_TEMPLATE_VERSIONS = ["1.0", CURRENT_TEMPLATE_VERSION];
347
+ var TemplateMigrationError = class extends Error {
348
+ constructor(code, message, options = {}) {
349
+ super(message);
350
+ this.name = "TemplateMigrationError";
351
+ this.code = code;
352
+ this.version = options.version;
353
+ this.issues = options.issues ?? [];
354
+ }
355
+ };
356
+ function isTemplateMigrationError(error) {
357
+ return error instanceof TemplateMigrationError;
358
+ }
359
+ function parseVersion(version) {
360
+ const match = /^(\d+)\.(\d+)$/.exec(version);
361
+ if (!match) return null;
362
+ return [Number(match[1]), Number(match[2])];
363
+ }
364
+ function compareVersions(a, b) {
365
+ return a[0] !== b[0] ? a[0] - b[0] : a[1] - b[1];
366
+ }
367
+ function migrateTemplate(doc) {
368
+ if (typeof doc !== "object" || doc === null || Array.isArray(doc)) {
369
+ throw new TemplateMigrationError(
370
+ "INVALID_INPUT",
371
+ `Expected a template document object, received ${doc === null ? "null" : Array.isArray(doc) ? "an array" : typeof doc}.`
372
+ );
373
+ }
374
+ const version = doc.version;
375
+ if (typeof version !== "string" || version.length === 0) {
376
+ throw new TemplateMigrationError(
377
+ "MISSING_VERSION",
378
+ 'The template document has no "version" field, so its schema cannot be determined.'
379
+ );
380
+ }
381
+ const parsed = parseVersion(version);
382
+ if (!parsed) {
383
+ throw new TemplateMigrationError(
384
+ "UNSUPPORTED_VERSION",
385
+ `Template schema version "${version}" is not of the form "major.minor".`,
386
+ { version }
387
+ );
388
+ }
389
+ const current = parseVersion(CURRENT_TEMPLATE_VERSION);
390
+ if (compareVersions(parsed, current) > 0) {
391
+ throw new TemplateMigrationError(
392
+ "NEWER_VERSION",
393
+ `Template schema version "${version}" is newer than this editor supports (${CURRENT_TEMPLATE_VERSION}). Upgrade the @marlinjai/email-editor packages to open it.`,
394
+ { version }
395
+ );
396
+ }
397
+ if (!SUPPORTED_TEMPLATE_VERSIONS.includes(version)) {
398
+ throw new TemplateMigrationError(
399
+ "UNSUPPORTED_VERSION",
400
+ `Template schema version "${version}" is not supported and has no migration to ${CURRENT_TEMPLATE_VERSION}.`,
401
+ { version }
402
+ );
403
+ }
404
+ let next = doc;
405
+ if (version === "1.0") {
406
+ assertValid(EmailTemplateSchemaV1_0, next, version);
407
+ next = migrateV1_0ToV1_1(next);
408
+ }
409
+ assertValid(EmailTemplateSchemaV1_1, next, version);
410
+ return next;
411
+ }
412
+ function assertValid(schema, doc, version) {
413
+ const result = schema.safeParse(doc);
414
+ if (result.success) return;
415
+ const issues = result.error.issues.map((issue) => ({ path: issue.path, message: issue.message }));
416
+ const first = issues[0];
417
+ throw new TemplateMigrationError(
418
+ "INVALID_DOCUMENT",
419
+ `Template document does not match schema version ${version}: ${first ? `${first.path.join(".") || "(root)"}: ${first.message}` : "unknown validation error"}${issues.length > 1 ? ` (and ${issues.length - 1} more)` : ""}.`,
420
+ { version, issues }
421
+ );
422
+ }
423
+ function migrateV1_0ToV1_1(doc) {
424
+ if (!doc.sections.some((s) => s.isWrapper === true && !s.bodyRaw)) {
425
+ return { ...doc, version: "1.1", sections: doc.sections.map(stripIsWrapper) };
426
+ }
427
+ const ids = /* @__PURE__ */ new Set();
428
+ for (const s of doc.sections) {
429
+ ids.add(s.id);
430
+ for (const c of s.columns ?? []) {
431
+ ids.add(c.id);
432
+ for (const b of c.blocks ?? []) ids.add(b.id);
433
+ }
434
+ }
435
+ const freshId = (base) => {
436
+ let candidate = `${base}-inner`;
437
+ for (let n = 2; ids.has(candidate); n++) candidate = `${base}-inner-${n}`;
438
+ ids.add(candidate);
439
+ return candidate;
440
+ };
441
+ const sections = doc.sections.map((s) => {
442
+ if (s.isWrapper !== true || s.bodyRaw) return stripIsWrapper(s);
443
+ const { isWrapper: _flag, noStack: _noStack, ...rest } = s;
444
+ const wrapper = { id: s.id, type: "wrapper", sections: [{ id: freshId(s.id), type: "section", columns: s.columns }] };
445
+ if (rest.hidden !== void 0) wrapper.hidden = rest.hidden;
446
+ if (rest.backgroundColor !== void 0) wrapper.backgroundColor = rest.backgroundColor;
447
+ if (rest.backgroundImage !== void 0) wrapper.backgroundImage = rest.backgroundImage;
448
+ if (rest.backgroundGradient !== void 0) wrapper.backgroundGradient = rest.backgroundGradient;
449
+ if (rest.backgroundPosition !== void 0) wrapper.backgroundPosition = rest.backgroundPosition;
450
+ if (rest.backgroundRepeat !== void 0) wrapper.backgroundRepeat = rest.backgroundRepeat;
451
+ if (rest.backgroundSize !== void 0) wrapper.backgroundSize = rest.backgroundSize;
452
+ if (rest.fullWidth !== void 0) wrapper.fullWidth = rest.fullWidth;
453
+ if (rest.padding !== void 0) wrapper.padding = rest.padding;
454
+ if (rest.extraAttributes !== void 0) wrapper.extraAttributes = rest.extraAttributes;
455
+ return wrapper;
456
+ });
457
+ return { ...doc, version: "1.1", sections };
458
+ }
459
+ function stripIsWrapper(s) {
460
+ if (!("isWrapper" in s)) return s;
461
+ const { isWrapper: _flag, ...rest } = s;
462
+ return rest;
463
+ }
464
+ function withTemplateId(doc) {
465
+ if (typeof doc.id === "string" && doc.id.length > 0) return doc;
466
+ return { ...doc, id: nanoid() };
467
+ }
468
+
469
+ // src/store/mst/models/BlockModel.ts
470
+ import { types } from "mobx-state-tree";
471
+
472
+ // src/store/mst/models/spacingSnapshot.ts
473
+ var SIDES = [
474
+ ["top", "paddingTop"],
475
+ ["right", "paddingRight"],
476
+ ["bottom", "paddingBottom"],
477
+ ["left", "paddingLeft"]
478
+ ];
479
+ function paddingIn(snapshot) {
480
+ if (!snapshot || typeof snapshot !== "object") return snapshot;
481
+ const { padding, ...rest } = snapshot;
482
+ if (padding === void 0) return snapshot;
483
+ const flat = rest;
484
+ if (padding && typeof padding === "object") {
485
+ for (const [side, field] of SIDES) {
486
+ const value = padding[side];
487
+ if (flat[field] === void 0 && typeof value === "string" && value !== "") flat[field] = value;
488
+ }
489
+ }
490
+ return flat;
491
+ }
492
+ function paddingOut(snapshot) {
493
+ if (!snapshot || typeof snapshot !== "object") return snapshot;
494
+ const { paddingTop, paddingRight, paddingBottom, paddingLeft, ...rest } = snapshot;
495
+ const values = {
496
+ top: paddingTop,
497
+ right: paddingRight,
498
+ bottom: paddingBottom,
499
+ left: paddingLeft
500
+ };
501
+ const padding = {};
502
+ for (const [side] of SIDES) {
503
+ const value = values[side];
504
+ if (typeof value === "string" && value !== "") padding[side] = value;
505
+ }
506
+ return Object.keys(padding).length > 0 ? { ...rest, padding } : rest;
507
+ }
508
+
509
+ // src/store/mst/models/filledDefaults.ts
510
+ var same = (a, b) => JSON.stringify(a) === JSON.stringify(b);
511
+ function fillDefaults(snapshot, defaults) {
512
+ if (!snapshot || typeof snapshot !== "object") return snapshot;
513
+ const s = { ...snapshot };
514
+ const filled = { ...s.filled ?? {} };
515
+ for (const [key, make] of Object.entries(defaults)) {
516
+ if (s[key] !== void 0) continue;
517
+ const value = make();
518
+ s[key] = value;
519
+ filled[key] = value;
520
+ }
521
+ if (Object.keys(filled).length > 0) s.filled = filled;
522
+ return s;
523
+ }
524
+ function dropFilled(snapshot) {
525
+ if (!snapshot || typeof snapshot !== "object") return snapshot;
526
+ const { filled, ...rest } = snapshot;
527
+ if (filled) {
528
+ for (const [key, value] of Object.entries(filled)) {
529
+ if (same(rest[key], value)) delete rest[key];
530
+ }
531
+ }
532
+ for (const key of Object.keys(rest)) if (rest[key] === void 0) delete rest[key];
533
+ return rest;
534
+ }
535
+ var empty = () => [];
536
+ var no = () => false;
537
+ var BLOCK_DEFAULTS = {
538
+ hidden: no,
539
+ content: () => "",
540
+ links: empty,
541
+ items: empty,
542
+ navLinks: empty,
543
+ hamburger: no,
544
+ images: empty,
545
+ headers: empty,
546
+ rows: empty,
547
+ locked: no
548
+ };
549
+ var COLUMN_DEFAULTS = {
550
+ width: () => 100,
551
+ hidden: no,
552
+ subColumns: empty
553
+ };
554
+ var SECTION_DEFAULTS = {
555
+ type: () => "section",
556
+ fullWidth: no,
557
+ noStack: no,
558
+ hidden: no
559
+ };
560
+ var WRAPPER_DEFAULTS = {
561
+ hidden: no,
562
+ fullWidth: no
563
+ };
564
+ function fillColumnWidths(section) {
565
+ if (!section || typeof section !== "object") return section;
566
+ const s = section;
567
+ if (!Array.isArray(s.columns) || s.columns.length === 0) return section;
568
+ if (s.columns.every((c) => c?.width !== void 0)) return section;
569
+ const share = 100 / s.columns.length;
570
+ return {
571
+ ...s,
572
+ columns: s.columns.map((c) => {
573
+ const col = c;
574
+ if (!col || typeof col !== "object" || col.width !== void 0) return c;
575
+ return { ...col, width: share, filled: { ...col.filled ?? {}, width: share } };
576
+ })
577
+ };
578
+ }
579
+
580
+ // src/store/mst/models/BlockModel.ts
581
+ var BlockType = /* @__PURE__ */ ((BlockType2) => {
582
+ BlockType2["TEXT"] = "text";
583
+ BlockType2["IMAGE"] = "image";
584
+ BlockType2["BUTTON"] = "button";
585
+ BlockType2["DIVIDER"] = "divider";
586
+ BlockType2["SPACER"] = "spacer";
587
+ BlockType2["SOCIAL"] = "social";
588
+ BlockType2["HERO"] = "hero";
589
+ BlockType2["ACCORDION"] = "accordion";
590
+ BlockType2["RAW"] = "raw";
591
+ BlockType2["NAVBAR"] = "navbar";
592
+ BlockType2["CAROUSEL"] = "carousel";
593
+ BlockType2["TABLE"] = "table";
594
+ BlockType2["HEADER"] = "header";
595
+ BlockType2["FOOTER"] = "footer";
596
+ return BlockType2;
597
+ })(BlockType || {});
598
+ var SocialLinkModel = types.model("SocialLink", {
599
+ platform: types.string,
600
+ url: types.string,
601
+ color: types.maybe(types.string)
602
+ });
603
+ var NavbarLinkModel = types.model("NavbarLink", {
604
+ href: types.string,
605
+ label: types.string,
606
+ color: types.maybe(types.string)
607
+ });
608
+ var CarouselImageModel = types.model("CarouselImage", {
609
+ src: types.string,
610
+ alt: types.maybe(types.string),
611
+ href: types.maybe(types.string),
612
+ thumbnailSrc: types.maybe(types.string)
613
+ });
614
+ var AccordionItemModel = types.model("AccordionItem", {
615
+ title: types.string,
616
+ content: types.string
617
+ });
618
+ var SpacingModel = types.model("Spacing", {
619
+ top: types.maybe(types.string),
620
+ right: types.maybe(types.string),
621
+ bottom: types.maybe(types.string),
622
+ left: types.maybe(types.string)
623
+ });
624
+ var BlockModelBase = types.model("Block", {
625
+ id: types.identifier,
626
+ type: types.enumeration("BlockType", Object.values(BlockType)),
627
+ hidden: types.optional(types.boolean, false),
628
+ // === Common Text Properties ===
629
+ content: types.optional(types.string, ""),
630
+ color: types.maybe(types.string),
631
+ backgroundColor: types.maybe(types.string),
632
+ fontSize: types.maybe(types.string),
633
+ fontFamily: types.maybe(types.string),
634
+ align: types.maybe(types.enumeration(["left", "center", "right", "justify"])),
635
+ lineHeight: types.maybe(types.string),
636
+ // === Spacing ===
637
+ paddingTop: types.maybe(types.string),
638
+ paddingRight: types.maybe(types.string),
639
+ paddingBottom: types.maybe(types.string),
640
+ paddingLeft: types.maybe(types.string),
641
+ // === Image Properties ===
642
+ src: types.maybe(types.string),
643
+ alt: types.maybe(types.string),
644
+ width: types.maybe(types.string),
645
+ height: types.maybe(types.string),
646
+ // === Button Properties ===
647
+ href: types.maybe(types.string),
648
+ label: types.maybe(types.string),
649
+ borderRadius: types.maybe(types.string),
650
+ border: types.maybe(types.string),
651
+ innerPadding: types.maybe(types.string),
652
+ // === Divider Properties ===
653
+ borderColor: types.maybe(types.string),
654
+ borderWidth: types.maybe(types.string),
655
+ borderStyle: types.maybe(types.enumeration(["solid", "dashed", "dotted"])),
656
+ // === Spacer Properties ===
657
+ // (uses height from image properties)
658
+ // === Social Properties ===
659
+ links: types.optional(types.array(SocialLinkModel), []),
660
+ iconSize: types.maybe(types.string),
661
+ iconPadding: types.maybe(types.string),
662
+ mode: types.maybe(types.enumeration(["horizontal", "vertical", "fixed-height", "fluid-height"])),
663
+ // === Hero Properties ===
664
+ backgroundImage: types.maybe(types.string),
665
+ backgroundHeight: types.maybe(types.string),
666
+ backgroundWidth: types.maybe(types.string),
667
+ verticalAlign: types.maybe(types.enumeration(["top", "middle", "bottom"])),
668
+ // === Accordion Properties ===
669
+ items: types.optional(types.array(AccordionItemModel), []),
670
+ iconPosition: types.maybe(types.enumeration(["left", "right"])),
671
+ // === Raw Properties ===
672
+ html: types.maybe(types.string),
673
+ // === Navbar Properties ===
674
+ navLinks: types.optional(types.array(NavbarLinkModel), []),
675
+ hamburger: types.optional(types.boolean, false),
676
+ baseUrl: types.maybe(types.string),
677
+ icoColor: types.maybe(types.string),
678
+ // === Carousel Properties ===
679
+ images: types.optional(types.array(CarouselImageModel), []),
680
+ thumbnails: types.maybe(types.enumeration(["visible", "hidden"])),
681
+ iconWidth: types.maybe(types.string),
682
+ tbBorderRadius: types.maybe(types.string),
683
+ // === Table Properties ===
684
+ headers: types.optional(types.array(types.string), []),
685
+ rows: types.optional(types.array(types.array(types.string)), []),
686
+ cellpadding: types.maybe(types.string),
687
+ cellspacing: types.maybe(types.string),
688
+ // === Locked Blocks (Header/Footer) ===
689
+ locked: types.optional(types.boolean, false),
690
+ // === MJML attributes the inspector has no control for (kept from an import) ===
691
+ extraAttributes: types.maybe(types.frozen()),
692
+ /** Defaults the store filled when it opened the node; they go back out only if changed (see `filledDefaults.ts`). */
693
+ filled: types.maybe(types.frozen())
694
+ }).actions((self) => ({
695
+ /**
696
+ * Update any style property by name
697
+ */
698
+ updateStyle(property, value) {
699
+ if (property in self) {
700
+ self[property] = value;
701
+ }
702
+ },
703
+ /**
704
+ * Update multiple properties at once
705
+ */
706
+ updateProperties(updates) {
707
+ Object.entries(updates).forEach(([key, value]) => {
708
+ if (key in self) {
709
+ self[key] = value;
710
+ }
711
+ });
712
+ },
713
+ /**
714
+ * Set block content (for text blocks)
715
+ */
716
+ setContent(content) {
717
+ self.content = content;
718
+ },
719
+ /**
720
+ * Toggle block visibility
721
+ */
722
+ toggleHidden() {
723
+ self.hidden = !self.hidden;
724
+ },
725
+ /**
726
+ * Set padding values
727
+ */
728
+ setPadding(padding) {
729
+ if (padding.top !== void 0) self.paddingTop = padding.top;
730
+ if (padding.right !== void 0) self.paddingRight = padding.right;
731
+ if (padding.bottom !== void 0) self.paddingBottom = padding.bottom;
732
+ if (padding.left !== void 0) self.paddingLeft = padding.left;
733
+ },
734
+ /**
735
+ * Add a social link
736
+ */
737
+ addSocialLink(platform, url, color) {
738
+ self.links.push(SocialLinkModel.create({ platform, url, color }));
739
+ },
740
+ /**
741
+ * Remove a social link by index
742
+ */
743
+ removeSocialLink(index) {
744
+ self.links.splice(index, 1);
745
+ },
746
+ /**
747
+ * Add a navbar link
748
+ */
749
+ addNavbarLink(href, label, color) {
750
+ self.navLinks.push(NavbarLinkModel.create({ href, label, color }));
751
+ },
752
+ /**
753
+ * Remove a navbar link by index
754
+ */
755
+ removeNavbarLink(index) {
756
+ self.navLinks.splice(index, 1);
757
+ },
758
+ /**
759
+ * Add a carousel image
760
+ */
761
+ addCarouselImage(src, alt, href, thumbnailSrc) {
762
+ self.images.push(CarouselImageModel.create({ src, alt, href, thumbnailSrc }));
763
+ },
764
+ /**
765
+ * Remove a carousel image by index
766
+ */
767
+ removeCarouselImage(index) {
768
+ self.images.splice(index, 1);
769
+ },
770
+ /**
771
+ * Add an accordion item
772
+ */
773
+ addAccordionItem(title, content) {
774
+ self.items.push(AccordionItemModel.create({ title, content }));
775
+ },
776
+ /**
777
+ * Remove an accordion item by index
778
+ */
779
+ removeAccordionItem(index) {
780
+ self.items.splice(index, 1);
781
+ },
782
+ /**
783
+ * Add a table row
784
+ */
785
+ addTableRow(cells) {
786
+ self.rows.push(cells);
787
+ },
788
+ /**
789
+ * Remove a table row by index
790
+ */
791
+ removeTableRow(index) {
792
+ self.rows.splice(index, 1);
793
+ }
794
+ })).views((self) => ({
795
+ /**
796
+ * Computed style object for React rendering
797
+ */
798
+ get computedStyle() {
799
+ const style = {};
800
+ if (self.color) style.color = self.color;
801
+ if (self.backgroundColor) style.backgroundColor = self.backgroundColor;
802
+ if (self.fontSize) style.fontSize = self.fontSize;
803
+ if (self.fontFamily) style.fontFamily = self.fontFamily;
804
+ if (self.align) style.textAlign = self.align;
805
+ if (self.lineHeight) style.lineHeight = self.lineHeight;
806
+ if (self.paddingTop) style.paddingTop = self.paddingTop;
807
+ if (self.paddingRight) style.paddingRight = self.paddingRight;
808
+ if (self.paddingBottom) style.paddingBottom = self.paddingBottom;
809
+ if (self.paddingLeft) style.paddingLeft = self.paddingLeft;
810
+ if (self.borderRadius) style.borderRadius = self.borderRadius;
811
+ if (self.border) style.border = self.border;
812
+ return style;
813
+ },
814
+ /**
815
+ * Get padding as a single object
816
+ */
817
+ get padding() {
818
+ return {
819
+ top: self.paddingTop || void 0,
820
+ right: self.paddingRight || void 0,
821
+ bottom: self.paddingBottom || void 0,
822
+ left: self.paddingLeft || void 0
823
+ };
824
+ },
825
+ /**
826
+ * Get padding as a CSS string (e.g., "10px 20px 10px 20px")
827
+ */
828
+ get paddingString() {
829
+ const { paddingTop, paddingRight, paddingBottom, paddingLeft } = self;
830
+ if (!paddingTop && !paddingRight && !paddingBottom && !paddingLeft) {
831
+ return void 0;
832
+ }
833
+ return `${paddingTop || "0"} ${paddingRight || "0"} ${paddingBottom || "0"} ${paddingLeft || "0"}`;
834
+ },
835
+ /**
836
+ * MJML attributes for export
837
+ */
838
+ get mjmlAttributes() {
839
+ const attrs = {};
840
+ if (self.color) attrs.color = self.color;
841
+ if (self.backgroundColor) attrs["background-color"] = self.backgroundColor;
842
+ if (self.fontSize) attrs["font-size"] = self.fontSize;
843
+ if (self.fontFamily) attrs["font-family"] = self.fontFamily;
844
+ if (self.align) attrs.align = self.align;
845
+ if (self.lineHeight) attrs["line-height"] = self.lineHeight;
846
+ const paddingParts = [];
847
+ if (self.paddingTop) paddingParts.push(`top:${self.paddingTop}`);
848
+ if (self.paddingRight) paddingParts.push(`right:${self.paddingRight}`);
849
+ if (self.paddingBottom) paddingParts.push(`bottom:${self.paddingBottom}`);
850
+ if (self.paddingLeft) paddingParts.push(`left:${self.paddingLeft}`);
851
+ if (paddingParts.length > 0) {
852
+ const padding = `${self.paddingTop || "0"} ${self.paddingRight || "0"} ${self.paddingBottom || "0"} ${self.paddingLeft || "0"}`;
853
+ attrs.padding = padding;
854
+ }
855
+ if (self.src) attrs.src = self.src;
856
+ if (self.alt) attrs.alt = self.alt;
857
+ if (self.width) attrs.width = self.width;
858
+ if (self.height) attrs.height = self.height;
859
+ if (self.href) attrs.href = self.href;
860
+ if (self.borderRadius) attrs["border-radius"] = self.borderRadius;
861
+ if (self.border) attrs.border = self.border;
862
+ if (self.innerPadding) attrs["inner-padding"] = self.innerPadding;
863
+ if (self.borderColor) attrs["border-color"] = self.borderColor;
864
+ if (self.borderWidth) attrs["border-width"] = self.borderWidth;
865
+ if (self.borderStyle) attrs["border-style"] = self.borderStyle;
866
+ return attrs;
867
+ },
868
+ /**
869
+ * Check if this is a text-based block
870
+ */
871
+ get isTextBlock() {
872
+ return self.type === "text" /* TEXT */;
873
+ },
874
+ /**
875
+ * Check if this is an image block
876
+ */
877
+ get isImageBlock() {
878
+ return self.type === "image" /* IMAGE */;
879
+ },
880
+ /**
881
+ * Check if this is a button block
882
+ */
883
+ get isButtonBlock() {
884
+ return self.type === "button" /* BUTTON */;
885
+ },
886
+ /**
887
+ * Check if this block is locked (header/footer)
888
+ */
889
+ get isLocked() {
890
+ return self.locked || self.type === "header" /* HEADER */ || self.type === "footer" /* FOOTER */;
891
+ },
892
+ /**
893
+ * Get display name for layers panel
894
+ */
895
+ get displayName() {
896
+ switch (self.type) {
897
+ case "text" /* TEXT */:
898
+ const text = self.content.replace(/<[^>]*>/g, "").trim();
899
+ return text.length > 20 ? text.substring(0, 20) + "..." : text || "Text";
900
+ case "image" /* IMAGE */:
901
+ return self.alt || "Image";
902
+ case "button" /* BUTTON */:
903
+ return self.label || "Button";
904
+ case "divider" /* DIVIDER */:
905
+ return "Divider";
906
+ case "spacer" /* SPACER */:
907
+ return `Spacer (${self.height || "20px"})`;
908
+ case "social" /* SOCIAL */:
909
+ return "Social Links";
910
+ case "hero" /* HERO */:
911
+ return "Hero";
912
+ case "accordion" /* ACCORDION */:
913
+ return "Accordion";
914
+ case "raw" /* RAW */:
915
+ return "Custom HTML";
916
+ case "navbar" /* NAVBAR */:
917
+ return "Navigation";
918
+ case "carousel" /* CAROUSEL */:
919
+ return "Carousel";
920
+ case "table" /* TABLE */:
921
+ return "Table";
922
+ case "header" /* HEADER */:
923
+ return "Header";
924
+ case "footer" /* FOOTER */:
925
+ return "Footer";
926
+ default:
927
+ return "Block";
928
+ }
929
+ }
930
+ }));
931
+ var BlockModel = BlockModelBase.preProcessSnapshot((raw) => {
932
+ let snapshot = paddingIn(raw);
933
+ if (snapshot && snapshot.type === "navbar") {
934
+ const { links, navLinks, ...rest } = snapshot;
935
+ const stored = navLinks && navLinks.length > 0 ? navLinks : links ?? [];
936
+ snapshot = { ...rest, navLinks: stored };
937
+ }
938
+ return fillDefaults(snapshot, BLOCK_DEFAULTS);
939
+ }).postProcessSnapshot((raw) => {
940
+ const snapshot = paddingOut(dropFilled(raw));
941
+ if (snapshot.type !== "navbar") return snapshot;
942
+ const { navLinks, links: _socialLinks, ...rest } = snapshot;
943
+ return { ...rest, links: navLinks ?? [] };
944
+ });
945
+
946
+ // src/registry/blockCategories.ts
947
+ var LEAF_BLOCK_TYPES = [
948
+ "text" /* TEXT */,
949
+ "image" /* IMAGE */,
950
+ "button" /* BUTTON */,
951
+ "divider" /* DIVIDER */,
952
+ "spacer" /* SPACER */,
953
+ "social" /* SOCIAL */
954
+ ];
955
+ var CONTAINER_BLOCK_TYPES = [
956
+ "hero" /* HERO */,
957
+ "accordion" /* ACCORDION */,
958
+ "raw" /* RAW */,
959
+ "navbar" /* NAVBAR */,
960
+ "carousel" /* CAROUSEL */,
961
+ "table" /* TABLE */,
962
+ "header" /* HEADER */,
963
+ "footer" /* FOOTER */
964
+ ];
965
+ function isLeafBlockType(t) {
966
+ return LEAF_BLOCK_TYPES.includes(t);
967
+ }
968
+
969
+ export {
970
+ buildGradientCSS,
971
+ isWrapper,
972
+ allSections,
973
+ SpacingSchema,
974
+ GradientStopSchema,
975
+ BackgroundGradientSchema,
976
+ ExtraAttributesSchema,
977
+ MjmlHeadSchema,
978
+ CustomFontSchema,
979
+ TemplateMetadataSchema,
980
+ TextBlockSchema,
981
+ ImageBlockSchema,
982
+ ButtonBlockSchema,
983
+ DividerBlockSchema,
984
+ SpacerBlockSchema,
985
+ HeaderBlockSchema,
986
+ FooterBlockSchema,
987
+ SocialBlockSchema,
988
+ HeroBlockSchema,
989
+ AccordionBlockSchema,
990
+ RawBlockSchema,
991
+ NavbarBlockSchema,
992
+ CarouselBlockSchema,
993
+ TableBlockSchema,
994
+ BlockSchema,
995
+ ColumnSchema,
996
+ SectionSchema,
997
+ SectionSchemaV1_0,
998
+ WrapperSchema,
999
+ TopLevelItemSchema,
1000
+ EmailTemplateSchemaV1_0,
1001
+ EmailTemplateSchemaV1_1,
1002
+ EmailTemplateSchema,
1003
+ validateTemplate,
1004
+ CURRENT_TEMPLATE_VERSION,
1005
+ SUPPORTED_TEMPLATE_VERSIONS,
1006
+ TemplateMigrationError,
1007
+ isTemplateMigrationError,
1008
+ migrateTemplate,
1009
+ migrateV1_0ToV1_1,
1010
+ withTemplateId,
1011
+ paddingIn,
1012
+ paddingOut,
1013
+ fillDefaults,
1014
+ dropFilled,
1015
+ COLUMN_DEFAULTS,
1016
+ SECTION_DEFAULTS,
1017
+ WRAPPER_DEFAULTS,
1018
+ fillColumnWidths,
1019
+ BlockType,
1020
+ SocialLinkModel,
1021
+ NavbarLinkModel,
1022
+ CarouselImageModel,
1023
+ AccordionItemModel,
1024
+ SpacingModel,
1025
+ BlockModel,
1026
+ LEAF_BLOCK_TYPES,
1027
+ CONTAINER_BLOCK_TYPES,
1028
+ isLeafBlockType
1029
+ };