@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.
package/dist/index.mjs ADDED
@@ -0,0 +1,2287 @@
1
+ import {
2
+ AccordionBlockSchema,
3
+ AccordionItemModel,
4
+ BackgroundGradientSchema,
5
+ BlockModel,
6
+ BlockSchema,
7
+ BlockType,
8
+ ButtonBlockSchema,
9
+ COLUMN_DEFAULTS,
10
+ CONTAINER_BLOCK_TYPES,
11
+ CURRENT_TEMPLATE_VERSION,
12
+ CarouselBlockSchema,
13
+ CarouselImageModel,
14
+ ColumnSchema,
15
+ CustomFontSchema,
16
+ DividerBlockSchema,
17
+ EmailTemplateSchema,
18
+ EmailTemplateSchemaV1_0,
19
+ EmailTemplateSchemaV1_1,
20
+ ExtraAttributesSchema,
21
+ FooterBlockSchema,
22
+ GradientStopSchema,
23
+ HeaderBlockSchema,
24
+ HeroBlockSchema,
25
+ ImageBlockSchema,
26
+ LEAF_BLOCK_TYPES,
27
+ MjmlHeadSchema,
28
+ NavbarBlockSchema,
29
+ NavbarLinkModel,
30
+ RawBlockSchema,
31
+ SECTION_DEFAULTS,
32
+ SUPPORTED_TEMPLATE_VERSIONS,
33
+ SectionSchema,
34
+ SectionSchemaV1_0,
35
+ SocialBlockSchema,
36
+ SocialLinkModel,
37
+ SpacerBlockSchema,
38
+ SpacingModel,
39
+ SpacingSchema,
40
+ TableBlockSchema,
41
+ TemplateMetadataSchema,
42
+ TemplateMigrationError,
43
+ TextBlockSchema,
44
+ TopLevelItemSchema,
45
+ WRAPPER_DEFAULTS,
46
+ WrapperSchema,
47
+ allSections,
48
+ buildGradientCSS,
49
+ dropFilled,
50
+ fillColumnWidths,
51
+ fillDefaults,
52
+ isLeafBlockType,
53
+ isTemplateMigrationError,
54
+ isWrapper,
55
+ migrateTemplate,
56
+ migrateV1_0ToV1_1,
57
+ paddingIn,
58
+ paddingOut,
59
+ validateTemplate,
60
+ withTemplateId
61
+ } from "./chunk-2ZL7A2I6.mjs";
62
+
63
+ // src/registry/BlockRegistry.ts
64
+ var BlockRegistryImpl = class {
65
+ constructor() {
66
+ this.definitions = /* @__PURE__ */ new Map();
67
+ }
68
+ /**
69
+ * Register a new block definition
70
+ */
71
+ register(definition) {
72
+ if (this.definitions.has(definition.type)) {
73
+ console.warn(`Block type "${definition.type}" is already registered. Overwriting.`);
74
+ }
75
+ this.definitions.set(definition.type, definition);
76
+ }
77
+ /**
78
+ * Unregister a block definition
79
+ */
80
+ unregister(type) {
81
+ this.definitions.delete(type);
82
+ }
83
+ /**
84
+ * Get a block definition by type
85
+ */
86
+ get(type) {
87
+ return this.definitions.get(type);
88
+ }
89
+ /**
90
+ * Get all registered block definitions
91
+ */
92
+ getAll() {
93
+ return Array.from(this.definitions.values());
94
+ }
95
+ /**
96
+ * Get blocks by category
97
+ */
98
+ getByCategory(category) {
99
+ return this.getAll().filter((def) => def.category === category);
100
+ }
101
+ /**
102
+ * Check if a block type is registered
103
+ */
104
+ has(type) {
105
+ return this.definitions.has(type);
106
+ }
107
+ };
108
+ function createBlockRegistry() {
109
+ return new BlockRegistryImpl();
110
+ }
111
+
112
+ // src/history/HistoryManager.ts
113
+ import { produce } from "immer";
114
+ var HistoryManager = class {
115
+ constructor(initialState, maxHistorySize = 50) {
116
+ this.history = [];
117
+ this.currentIndex = -1;
118
+ this.maxHistorySize = maxHistorySize;
119
+ this.history.push(initialState);
120
+ this.currentIndex = 0;
121
+ }
122
+ /**
123
+ * Get current state
124
+ */
125
+ getCurrentState() {
126
+ return this.history[this.currentIndex];
127
+ }
128
+ /**
129
+ * Update state with a producer function (Immer)
130
+ * Adds new state to history
131
+ */
132
+ updateState(producer) {
133
+ const newState = produce(this.getCurrentState(), producer);
134
+ if (this.currentIndex < this.history.length - 1) {
135
+ this.history = this.history.slice(0, this.currentIndex + 1);
136
+ }
137
+ this.history.push(newState);
138
+ if (this.history.length > this.maxHistorySize) {
139
+ this.history = this.history.slice(-this.maxHistorySize);
140
+ this.currentIndex = this.history.length - 1;
141
+ } else {
142
+ this.currentIndex++;
143
+ }
144
+ return newState;
145
+ }
146
+ /**
147
+ * Replace current state without affecting history
148
+ */
149
+ replaceState(newState) {
150
+ this.history[this.currentIndex] = newState;
151
+ }
152
+ /**
153
+ * Undo to previous state
154
+ */
155
+ undo() {
156
+ if (!this.canUndo()) {
157
+ return null;
158
+ }
159
+ this.currentIndex--;
160
+ return this.getCurrentState();
161
+ }
162
+ /**
163
+ * Redo to next state
164
+ */
165
+ redo() {
166
+ if (!this.canRedo()) {
167
+ return null;
168
+ }
169
+ this.currentIndex++;
170
+ return this.getCurrentState();
171
+ }
172
+ /**
173
+ * Check if undo is available
174
+ */
175
+ canUndo() {
176
+ return this.currentIndex > 0;
177
+ }
178
+ /**
179
+ * Check if redo is available
180
+ */
181
+ canRedo() {
182
+ return this.currentIndex < this.history.length - 1;
183
+ }
184
+ /**
185
+ * Clear all history and set new initial state
186
+ */
187
+ reset(initialState) {
188
+ this.history = [initialState];
189
+ this.currentIndex = 0;
190
+ }
191
+ /**
192
+ * Get history size
193
+ */
194
+ getHistorySize() {
195
+ return this.history.length;
196
+ }
197
+ /**
198
+ * Get current index
199
+ */
200
+ getCurrentIndex() {
201
+ return this.currentIndex;
202
+ }
203
+ };
204
+ function createHistoryManager(initialState, maxHistorySize = 50) {
205
+ return new HistoryManager(initialState, maxHistorySize);
206
+ }
207
+
208
+ // src/selection/SelectionManager.ts
209
+ var SelectionManager = class {
210
+ constructor() {
211
+ this.state = {
212
+ blockId: null,
213
+ sectionId: null
214
+ };
215
+ this.listeners = [];
216
+ }
217
+ /**
218
+ * Get current selection
219
+ */
220
+ getSelection() {
221
+ return { ...this.state };
222
+ }
223
+ /**
224
+ * Select a block
225
+ */
226
+ selectBlock(blockId, sectionId) {
227
+ this.state = { blockId, sectionId };
228
+ this.notify();
229
+ }
230
+ /**
231
+ * Select a section
232
+ */
233
+ selectSection(sectionId) {
234
+ this.state = { blockId: null, sectionId };
235
+ this.notify();
236
+ }
237
+ /**
238
+ * Clear selection
239
+ */
240
+ clearSelection() {
241
+ this.state = { blockId: null, sectionId: null };
242
+ this.notify();
243
+ }
244
+ /**
245
+ * Subscribe to selection changes
246
+ */
247
+ subscribe(listener) {
248
+ this.listeners.push(listener);
249
+ return () => {
250
+ this.listeners = this.listeners.filter((l) => l !== listener);
251
+ };
252
+ }
253
+ /**
254
+ * Notify all listeners
255
+ */
256
+ notify() {
257
+ this.listeners.forEach((listener) => listener(this.state));
258
+ }
259
+ };
260
+ function createSelectionManager() {
261
+ return new SelectionManager();
262
+ }
263
+
264
+ // src/templates/registry.ts
265
+ var PrebuiltTemplateRegistryImpl = class {
266
+ constructor() {
267
+ this.templates = /* @__PURE__ */ new Map();
268
+ }
269
+ /**
270
+ * Register a pre-built template
271
+ */
272
+ register(template) {
273
+ this.templates.set(template.id, template);
274
+ }
275
+ /**
276
+ * Get a template by ID
277
+ */
278
+ get(id) {
279
+ return this.templates.get(id);
280
+ }
281
+ /**
282
+ * Get all registered templates
283
+ */
284
+ getAll() {
285
+ return Array.from(this.templates.values());
286
+ }
287
+ /**
288
+ * Get templates filtered by category
289
+ */
290
+ getByCategory(category) {
291
+ return this.getAll().filter((t) => t.category === category);
292
+ }
293
+ };
294
+ function createPrebuiltTemplateRegistry() {
295
+ return new PrebuiltTemplateRegistryImpl();
296
+ }
297
+
298
+ // src/store/mst/models/ColumnModel.ts
299
+ import { types as types2, destroy as destroy2, detach as detach2, isStateTreeNode as isStateTreeNode2 } from "mobx-state-tree";
300
+ import { nanoid as nanoid2 } from "nanoid";
301
+
302
+ // src/store/mst/models/SubColumnModel.ts
303
+ import { types, destroy, detach, isStateTreeNode } from "mobx-state-tree";
304
+ import { nanoid } from "nanoid";
305
+ var SubColumnModel = types.model("SubColumn", {
306
+ id: types.identifier,
307
+ width: types.optional(types.number, 50),
308
+ backgroundColor: types.maybe(types.string),
309
+ backgroundGradient: types.maybe(types.frozen()),
310
+ verticalAlign: types.maybe(types.enumeration(["top", "middle", "bottom"])),
311
+ paddingTop: types.maybe(types.string),
312
+ paddingRight: types.maybe(types.string),
313
+ paddingBottom: types.maybe(types.string),
314
+ paddingLeft: types.maybe(types.string),
315
+ blocks: types.array(BlockModel)
316
+ }).actions((self) => ({
317
+ addBlock(block, index) {
318
+ const type = block.type;
319
+ if (!isLeafBlockType(type)) {
320
+ throw new Error(`SubColumn only accepts leaf blocks; got "${type}"`);
321
+ }
322
+ const blockToAdd = isStateTreeNode(block) ? detach(block) : BlockModel.create(block);
323
+ if (index !== void 0 && index >= 0 && index <= self.blocks.length) {
324
+ self.blocks.splice(index, 0, blockToAdd);
325
+ } else {
326
+ self.blocks.push(blockToAdd);
327
+ }
328
+ return blockToAdd;
329
+ },
330
+ removeBlock(blockId) {
331
+ const block = self.blocks.find((b) => b.id === blockId);
332
+ if (block) {
333
+ destroy(block);
334
+ return true;
335
+ }
336
+ return false;
337
+ },
338
+ moveBlock(fromIndex, toIndex) {
339
+ if (fromIndex < 0 || fromIndex >= self.blocks.length) return false;
340
+ if (toIndex < 0 || toIndex > self.blocks.length) return false;
341
+ if (fromIndex === toIndex) return false;
342
+ const [block] = self.blocks.splice(fromIndex, 1);
343
+ const adj = toIndex > fromIndex ? toIndex - 1 : toIndex;
344
+ self.blocks.splice(adj, 0, block);
345
+ return true;
346
+ },
347
+ detachBlock(blockId) {
348
+ const block = self.blocks.find((b) => b.id === blockId);
349
+ return block ? detach(block) : void 0;
350
+ },
351
+ setWidth(width) {
352
+ self.width = Math.max(0, Math.min(100, width));
353
+ },
354
+ setPadding(p) {
355
+ if (p.top !== void 0) self.paddingTop = p.top;
356
+ if (p.right !== void 0) self.paddingRight = p.right;
357
+ if (p.bottom !== void 0) self.paddingBottom = p.bottom;
358
+ if (p.left !== void 0) self.paddingLeft = p.left;
359
+ },
360
+ updateProperties(updates) {
361
+ Object.entries(updates).forEach(([k, v]) => {
362
+ if (k in self) self[k] = v;
363
+ });
364
+ },
365
+ clearBlocks() {
366
+ self.blocks.forEach((b) => destroy(b));
367
+ self.blocks.clear();
368
+ }
369
+ })).views((self) => ({
370
+ get isEmpty() {
371
+ return self.blocks.length === 0;
372
+ },
373
+ get computedStyle() {
374
+ const style = { width: `${self.width}%` };
375
+ if (self.backgroundGradient) {
376
+ const css = buildGradientCSS(self.backgroundGradient);
377
+ if (css) style.backgroundImage = css;
378
+ }
379
+ if (self.backgroundColor) style.backgroundColor = self.backgroundColor;
380
+ if (self.verticalAlign) style.verticalAlign = self.verticalAlign;
381
+ if (self.paddingTop) style.paddingTop = self.paddingTop;
382
+ if (self.paddingRight) style.paddingRight = self.paddingRight;
383
+ if (self.paddingBottom) style.paddingBottom = self.paddingBottom;
384
+ if (self.paddingLeft) style.paddingLeft = self.paddingLeft;
385
+ return style;
386
+ }
387
+ }));
388
+ function createSubColumn(opts = {}) {
389
+ return {
390
+ id: opts.id ?? nanoid(),
391
+ width: opts.width ?? 50,
392
+ blocks: opts.blocks ?? []
393
+ };
394
+ }
395
+
396
+ // src/store/mst/models/ColumnModel.ts
397
+ var ColumnModelBase = types2.model("Column", {
398
+ id: types2.identifier,
399
+ width: types2.optional(types2.number, 100),
400
+ // percentage (e.g., 50 for 50%)
401
+ backgroundColor: types2.maybe(types2.string),
402
+ backgroundGradient: types2.maybe(types2.frozen()),
403
+ verticalAlign: types2.maybe(types2.enumeration(["top", "middle", "bottom"])),
404
+ paddingTop: types2.maybe(types2.string),
405
+ paddingRight: types2.maybe(types2.string),
406
+ paddingBottom: types2.maybe(types2.string),
407
+ paddingLeft: types2.maybe(types2.string),
408
+ hidden: types2.optional(types2.boolean, false),
409
+ blocks: types2.array(BlockModel),
410
+ /**
411
+ * Optional sub-columns for depth-2 nesting.
412
+ * Invariant: blocks XOR subColumns. Both can be empty, but never both populated.
413
+ *
414
+ * `types.late` is used to break TypeScript's deep inference, which otherwise
415
+ * makes the inferred RootStore type exceed the compiler's serialization limit.
416
+ */
417
+ subColumns: types2.optional(types2.array(types2.late(() => SubColumnModel)), []),
418
+ /** MJML attributes the inspector has no control for (kept from an import). */
419
+ extraAttributes: types2.maybe(types2.frozen()),
420
+ /** Defaults the store filled when it opened the node; they go back out only if changed (see `filledDefaults.ts`). */
421
+ filled: types2.maybe(types2.frozen())
422
+ }).preProcessSnapshot((snapshot) => {
423
+ if (snapshot && Array.isArray(snapshot.blocks) && snapshot.blocks.length > 0 && Array.isArray(snapshot.subColumns) && snapshot.subColumns.length > 0) {
424
+ throw new Error(
425
+ "Invalid Column snapshot: cannot have both blocks and subColumns populated"
426
+ );
427
+ }
428
+ return snapshot;
429
+ }).actions((self) => ({
430
+ /**
431
+ * Add a block to this column
432
+ */
433
+ addBlock(block, index) {
434
+ if (self.subColumns.length > 0) {
435
+ throw new Error("Cannot add block to a group column; merge sub-columns first");
436
+ }
437
+ const blockToAdd = isStateTreeNode2(block) ? detach2(block) : BlockModel.create(block);
438
+ if (index !== void 0 && index >= 0 && index <= self.blocks.length) {
439
+ self.blocks.splice(index, 0, blockToAdd);
440
+ } else {
441
+ self.blocks.push(blockToAdd);
442
+ }
443
+ return blockToAdd;
444
+ },
445
+ /**
446
+ * Remove a block by ID
447
+ */
448
+ removeBlock(blockId) {
449
+ const block = self.blocks.find((b) => b.id === blockId);
450
+ if (block) {
451
+ destroy2(block);
452
+ return true;
453
+ }
454
+ return false;
455
+ },
456
+ /**
457
+ * Move a block within this column
458
+ */
459
+ moveBlock(fromIndex, toIndex) {
460
+ if (fromIndex < 0 || fromIndex >= self.blocks.length) return false;
461
+ if (toIndex < 0 || toIndex > self.blocks.length) return false;
462
+ if (fromIndex === toIndex) return false;
463
+ const [block] = self.blocks.splice(fromIndex, 1);
464
+ const adjustedToIndex = toIndex > fromIndex ? toIndex - 1 : toIndex;
465
+ self.blocks.splice(adjustedToIndex, 0, block);
466
+ return true;
467
+ },
468
+ /**
469
+ * Detach a block (remove without destroying, for moving to another column)
470
+ */
471
+ detachBlock(blockId) {
472
+ const block = self.blocks.find((b) => b.id === blockId);
473
+ if (block) {
474
+ return detach2(block);
475
+ }
476
+ return void 0;
477
+ },
478
+ /**
479
+ * Update column properties
480
+ */
481
+ updateProperties(updates) {
482
+ Object.entries(updates).forEach(([key, value]) => {
483
+ if (key in self) {
484
+ self[key] = value;
485
+ }
486
+ });
487
+ },
488
+ /**
489
+ * Set padding
490
+ */
491
+ setPadding(padding) {
492
+ if (padding.top !== void 0) self.paddingTop = padding.top;
493
+ if (padding.right !== void 0) self.paddingRight = padding.right;
494
+ if (padding.bottom !== void 0) self.paddingBottom = padding.bottom;
495
+ if (padding.left !== void 0) self.paddingLeft = padding.left;
496
+ },
497
+ /**
498
+ * Toggle column visibility
499
+ */
500
+ toggleHidden() {
501
+ self.hidden = !self.hidden;
502
+ },
503
+ /**
504
+ * Set column width
505
+ */
506
+ setWidth(width) {
507
+ self.width = Math.max(0, Math.min(100, width));
508
+ },
509
+ /**
510
+ * Clear all blocks
511
+ */
512
+ clearBlocks() {
513
+ self.blocks.forEach((block) => destroy2(block));
514
+ self.blocks.clear();
515
+ },
516
+ /**
517
+ * Convert this leaf column into a group of N sub-columns (2-4).
518
+ * Existing blocks migrate into the first sub-column.
519
+ */
520
+ splitIntoSubColumns(count) {
521
+ if (count < 2 || count > 4) {
522
+ throw new Error(`splitIntoSubColumns requires count in [2..4], got ${count}`);
523
+ }
524
+ const firstBlocks = self.blocks.map((b) => detach2(b));
525
+ self.blocks.clear();
526
+ self.subColumns.clear();
527
+ const base = Math.floor(100 / count * 100) / 100;
528
+ for (let i = 0; i < count; i++) {
529
+ const isLast = i === count - 1;
530
+ const w = isLast ? Math.round((100 - base * (count - 1)) * 100) / 100 : base;
531
+ self.subColumns.push(
532
+ SubColumnModel.create(createSubColumn({
533
+ width: w,
534
+ blocks: i === 0 ? firstBlocks : []
535
+ }))
536
+ );
537
+ }
538
+ },
539
+ /**
540
+ * Merge all sub-column blocks back into self.blocks (concatenated in order).
541
+ */
542
+ mergeSubColumns() {
543
+ const collected = [];
544
+ for (const sc of self.subColumns) {
545
+ while (sc.blocks.length > 0) {
546
+ const b = detach2(sc.blocks[0]);
547
+ collected.push(b);
548
+ }
549
+ }
550
+ self.subColumns.clear();
551
+ for (const b of collected) {
552
+ self.blocks.push(b);
553
+ }
554
+ }
555
+ })).views((self) => ({
556
+ /**
557
+ * Whether this column is a leaf (holds blocks) or a group (holds sub-columns).
558
+ */
559
+ get kind() {
560
+ return self.subColumns.length > 0 ? "group" : "leaf";
561
+ },
562
+ /**
563
+ * Find a block by ID
564
+ */
565
+ getBlockById(blockId) {
566
+ return self.blocks.find((b) => b.id === blockId);
567
+ },
568
+ /**
569
+ * Get block index
570
+ */
571
+ getBlockIndex(blockId) {
572
+ return self.blocks.findIndex((b) => b.id === blockId);
573
+ },
574
+ /**
575
+ * Get visible blocks only
576
+ */
577
+ get visibleBlocks() {
578
+ return self.blocks.filter((b) => !b.hidden);
579
+ },
580
+ /**
581
+ * Count of blocks
582
+ */
583
+ get blockCount() {
584
+ return self.blocks.length;
585
+ },
586
+ /**
587
+ * Check if column is empty
588
+ */
589
+ get isEmpty() {
590
+ return self.blocks.length === 0;
591
+ },
592
+ /**
593
+ * Get padding as object
594
+ */
595
+ get padding() {
596
+ return {
597
+ top: self.paddingTop || void 0,
598
+ right: self.paddingRight || void 0,
599
+ bottom: self.paddingBottom || void 0,
600
+ left: self.paddingLeft || void 0
601
+ };
602
+ },
603
+ /**
604
+ * Get padding as CSS string
605
+ */
606
+ get paddingString() {
607
+ const { paddingTop, paddingRight, paddingBottom, paddingLeft } = self;
608
+ if (!paddingTop && !paddingRight && !paddingBottom && !paddingLeft) {
609
+ return void 0;
610
+ }
611
+ return `${paddingTop || "0"} ${paddingRight || "0"} ${paddingBottom || "0"} ${paddingLeft || "0"}`;
612
+ },
613
+ /**
614
+ * Computed style for rendering
615
+ */
616
+ get computedStyle() {
617
+ const style = {
618
+ width: `${self.width}%`
619
+ };
620
+ if (self.backgroundGradient) {
621
+ const css = buildGradientCSS(self.backgroundGradient);
622
+ if (css) style.backgroundImage = css;
623
+ }
624
+ if (self.backgroundColor) style.backgroundColor = self.backgroundColor;
625
+ if (self.verticalAlign) style.verticalAlign = self.verticalAlign;
626
+ if (self.paddingTop) style.paddingTop = self.paddingTop;
627
+ if (self.paddingRight) style.paddingRight = self.paddingRight;
628
+ if (self.paddingBottom) style.paddingBottom = self.paddingBottom;
629
+ if (self.paddingLeft) style.paddingLeft = self.paddingLeft;
630
+ return style;
631
+ },
632
+ /**
633
+ * MJML attributes for export
634
+ */
635
+ get mjmlAttributes() {
636
+ const attrs = {};
637
+ if (self.width !== 100) attrs.width = `${self.width}%`;
638
+ if (self.backgroundColor) attrs["background-color"] = self.backgroundColor;
639
+ if (self.verticalAlign) attrs["vertical-align"] = self.verticalAlign;
640
+ const hasPadding = self.paddingTop || self.paddingRight || self.paddingBottom || self.paddingLeft;
641
+ if (hasPadding) {
642
+ const padding = `${self.paddingTop || "0"} ${self.paddingRight || "0"} ${self.paddingBottom || "0"} ${self.paddingLeft || "0"}`;
643
+ attrs.padding = padding;
644
+ }
645
+ return attrs;
646
+ }
647
+ }));
648
+ var ColumnModel = ColumnModelBase.preProcessSnapshot(
649
+ (snapshot) => fillDefaults(paddingIn(snapshot), COLUMN_DEFAULTS)
650
+ ).postProcessSnapshot((snapshot) => paddingOut(dropFilled(snapshot)));
651
+ function createColumn(options = {}) {
652
+ return {
653
+ id: options.id || nanoid2(),
654
+ width: options.width || 100,
655
+ blocks: options.blocks || []
656
+ };
657
+ }
658
+
659
+ // src/store/mst/models/SectionModel.ts
660
+ import { types as types3, destroy as destroy3, detach as detach3 } from "mobx-state-tree";
661
+ import { nanoid as nanoid3 } from "nanoid";
662
+ var SectionModelBase = types3.model("Section", {
663
+ id: types3.identifier,
664
+ type: types3.optional(types3.literal("section"), "section"),
665
+ // Background
666
+ backgroundColor: types3.maybe(types3.string),
667
+ backgroundImage: types3.maybe(types3.string),
668
+ backgroundGradient: types3.maybe(
669
+ types3.model("BackgroundGradient", {
670
+ type: types3.enumeration(["linear", "radial"]),
671
+ angle: types3.number,
672
+ stops: types3.array(
673
+ types3.model("GradientStop", {
674
+ color: types3.string,
675
+ position: types3.number
676
+ })
677
+ )
678
+ })
679
+ ),
680
+ backgroundPosition: types3.maybe(types3.string),
681
+ backgroundRepeat: types3.maybe(types3.enumeration(["repeat", "no-repeat"])),
682
+ backgroundSize: types3.maybe(types3.string),
683
+ // Layout
684
+ fullWidth: types3.optional(types3.boolean, false),
685
+ noStack: types3.optional(types3.boolean, false),
686
+ // mj-group behavior
687
+ // Visibility
688
+ hidden: types3.optional(types3.boolean, false),
689
+ // Padding
690
+ paddingTop: types3.maybe(types3.string),
691
+ paddingRight: types3.maybe(types3.string),
692
+ paddingBottom: types3.maybe(types3.string),
693
+ paddingLeft: types3.maybe(types3.string),
694
+ // Columns
695
+ columns: types3.array(ColumnModel),
696
+ /**
697
+ * Emit the section's raw blocks straight into its parent (mj-body or
698
+ * mj-wrapper) instead of wrapping them in an mj-section (set by the MJML
699
+ * import for markup that sat there). Ignored once the section holds any
700
+ * other block.
701
+ */
702
+ bodyRaw: types3.maybe(types3.boolean),
703
+ /** MJML attributes the inspector has no control for (kept from an import). */
704
+ extraAttributes: types3.maybe(types3.frozen()),
705
+ /** Defaults the store filled when it opened the node; they go back out only if changed (see `filledDefaults.ts`). */
706
+ filled: types3.maybe(types3.frozen())
707
+ }).actions((self) => ({
708
+ /**
709
+ * Add a column to this section
710
+ */
711
+ addColumn(column, index) {
712
+ const columnToAdd = ColumnModel.is(column) ? detach3(column) : ColumnModel.create(column);
713
+ if (index !== void 0 && index >= 0 && index <= self.columns.length) {
714
+ self.columns.splice(index, 0, columnToAdd);
715
+ } else {
716
+ self.columns.push(columnToAdd);
717
+ }
718
+ this.rebalanceColumnWidths();
719
+ return columnToAdd;
720
+ },
721
+ /**
722
+ * Remove a column by ID
723
+ */
724
+ removeColumn(columnId) {
725
+ const column = self.columns.find((c) => c.id === columnId);
726
+ if (column && self.columns.length > 1) {
727
+ destroy3(column);
728
+ this.rebalanceColumnWidths();
729
+ return true;
730
+ }
731
+ return false;
732
+ },
733
+ /**
734
+ * Set the number of columns (1-4)
735
+ */
736
+ setColumnCount(count) {
737
+ const currentCount = self.columns.length;
738
+ if (count === currentCount) return;
739
+ if (count > currentCount) {
740
+ for (let i = currentCount; i < count; i++) {
741
+ self.columns.push(ColumnModel.create(createColumn()));
742
+ }
743
+ } else {
744
+ const blocksToMove = [];
745
+ for (let i = count; i < currentCount; i++) {
746
+ const column = self.columns[i];
747
+ column.blocks.forEach((block) => {
748
+ blocksToMove.push(detach3(block));
749
+ });
750
+ }
751
+ while (self.columns.length > count) {
752
+ const column = self.columns[self.columns.length - 1];
753
+ destroy3(column);
754
+ }
755
+ blocksToMove.forEach((block) => {
756
+ self.columns[0].blocks.push(block);
757
+ });
758
+ }
759
+ this.rebalanceColumnWidths();
760
+ },
761
+ /**
762
+ * Rebalance column widths to equal distribution
763
+ */
764
+ rebalanceColumnWidths() {
765
+ const columnCount = self.columns.length;
766
+ if (columnCount === 0) return;
767
+ const equalWidth = Math.floor(100 / columnCount);
768
+ self.columns.forEach((column, index) => {
769
+ if (index === columnCount - 1) {
770
+ column.setWidth(100 - equalWidth * (columnCount - 1));
771
+ } else {
772
+ column.setWidth(equalWidth);
773
+ }
774
+ });
775
+ },
776
+ /**
777
+ * Update section properties
778
+ */
779
+ updateProperties(updates) {
780
+ Object.entries(updates).forEach(([key, value]) => {
781
+ if (key in self) {
782
+ self[key] = value;
783
+ }
784
+ });
785
+ },
786
+ /**
787
+ * Set padding
788
+ */
789
+ setPadding(padding) {
790
+ if (padding.top !== void 0) self.paddingTop = padding.top;
791
+ if (padding.right !== void 0) self.paddingRight = padding.right;
792
+ if (padding.bottom !== void 0) self.paddingBottom = padding.bottom;
793
+ if (padding.left !== void 0) self.paddingLeft = padding.left;
794
+ },
795
+ /**
796
+ * Toggle section visibility
797
+ */
798
+ toggleHidden() {
799
+ self.hidden = !self.hidden;
800
+ },
801
+ /**
802
+ * Toggle full width
803
+ */
804
+ toggleFullWidth() {
805
+ self.fullWidth = !self.fullWidth;
806
+ },
807
+ /**
808
+ * Toggle no-stack (mj-group)
809
+ */
810
+ toggleNoStack() {
811
+ self.noStack = !self.noStack;
812
+ },
813
+ /**
814
+ * Move a block from one column to another within this section
815
+ */
816
+ moveBlockBetweenColumns(blockId, sourceColumnId, targetColumnId, targetIndex) {
817
+ const sourceColumn = self.columns.find((c) => c.id === sourceColumnId);
818
+ const targetColumn = self.columns.find((c) => c.id === targetColumnId);
819
+ if (!sourceColumn || !targetColumn) return false;
820
+ const block = sourceColumn.detachBlock(blockId);
821
+ if (!block) return false;
822
+ targetColumn.addBlock(block, targetIndex);
823
+ return true;
824
+ }
825
+ })).views((self) => ({
826
+ /**
827
+ * Find a column by ID
828
+ */
829
+ getColumnById(columnId) {
830
+ return self.columns.find((c) => c.id === columnId);
831
+ },
832
+ /**
833
+ * Find a block by ID (searches all columns)
834
+ */
835
+ findBlockById(blockId) {
836
+ for (const column of self.columns) {
837
+ const block = column.getBlockById(blockId);
838
+ if (block) return block;
839
+ }
840
+ return void 0;
841
+ },
842
+ /**
843
+ * Find which column contains a block
844
+ */
845
+ findColumnByBlockId(blockId) {
846
+ for (const column of self.columns) {
847
+ if (column.getBlockById(blockId)) {
848
+ return column;
849
+ }
850
+ }
851
+ return void 0;
852
+ },
853
+ /**
854
+ * Get column index
855
+ */
856
+ getColumnIndex(columnId) {
857
+ return self.columns.findIndex((c) => c.id === columnId);
858
+ },
859
+ /**
860
+ * Get visible columns
861
+ */
862
+ get visibleColumns() {
863
+ return self.columns.filter((c) => !c.hidden);
864
+ },
865
+ /**
866
+ * Column count
867
+ */
868
+ get columnCount() {
869
+ return self.columns.length;
870
+ },
871
+ /**
872
+ * Total block count across all columns
873
+ */
874
+ get totalBlockCount() {
875
+ return self.columns.reduce((sum, col) => sum + col.blockCount, 0);
876
+ },
877
+ /**
878
+ * Check if section is empty (all columns are empty)
879
+ */
880
+ get isEmpty() {
881
+ return self.columns.every((c) => c.isEmpty);
882
+ },
883
+ /**
884
+ * Get padding as object
885
+ */
886
+ get padding() {
887
+ return {
888
+ top: self.paddingTop || void 0,
889
+ right: self.paddingRight || void 0,
890
+ bottom: self.paddingBottom || void 0,
891
+ left: self.paddingLeft || void 0
892
+ };
893
+ },
894
+ /**
895
+ * Get padding as CSS string
896
+ */
897
+ get paddingString() {
898
+ const { paddingTop, paddingRight, paddingBottom, paddingLeft } = self;
899
+ if (!paddingTop && !paddingRight && !paddingBottom && !paddingLeft) {
900
+ return void 0;
901
+ }
902
+ return `${paddingTop || "0"} ${paddingRight || "0"} ${paddingBottom || "0"} ${paddingLeft || "0"}`;
903
+ },
904
+ /**
905
+ * Computed style for rendering
906
+ */
907
+ get computedStyle() {
908
+ const style = {};
909
+ if (self.backgroundColor) style.backgroundColor = self.backgroundColor;
910
+ const gradientCSS = self.backgroundGradient ? buildGradientCSS(self.backgroundGradient) : void 0;
911
+ if (gradientCSS) {
912
+ style.backgroundImage = gradientCSS;
913
+ } else if (self.backgroundImage) {
914
+ style.backgroundImage = `url(${self.backgroundImage})`;
915
+ if (self.backgroundPosition) style.backgroundPosition = self.backgroundPosition;
916
+ if (self.backgroundRepeat) style.backgroundRepeat = self.backgroundRepeat;
917
+ if (self.backgroundSize) style.backgroundSize = self.backgroundSize;
918
+ }
919
+ if (self.paddingTop) style.paddingTop = self.paddingTop;
920
+ if (self.paddingRight) style.paddingRight = self.paddingRight;
921
+ if (self.paddingBottom) style.paddingBottom = self.paddingBottom;
922
+ if (self.paddingLeft) style.paddingLeft = self.paddingLeft;
923
+ return style;
924
+ },
925
+ /**
926
+ * MJML attributes for export
927
+ */
928
+ get mjmlAttributes() {
929
+ const attrs = {};
930
+ if (self.backgroundColor) attrs["background-color"] = self.backgroundColor;
931
+ if (self.backgroundImage) attrs["background-url"] = self.backgroundImage;
932
+ if (self.backgroundPosition) attrs["background-position"] = self.backgroundPosition;
933
+ if (self.backgroundRepeat) attrs["background-repeat"] = self.backgroundRepeat;
934
+ if (self.backgroundSize) attrs["background-size"] = self.backgroundSize;
935
+ if (self.fullWidth) attrs["full-width"] = "full-width";
936
+ const hasPadding = self.paddingTop || self.paddingRight || self.paddingBottom || self.paddingLeft;
937
+ if (hasPadding) {
938
+ const padding = `${self.paddingTop || "0"} ${self.paddingRight || "0"} ${self.paddingBottom || "0"} ${self.paddingLeft || "0"}`;
939
+ attrs.padding = padding;
940
+ }
941
+ return attrs;
942
+ },
943
+ /**
944
+ * How wide the section's visible columns are together, in percent, as MJML
945
+ * lays them out: each column's width, where a column opened without one
946
+ * holds MJML's share (100 / columns, whatever its siblings say). Over 100,
947
+ * the last column wraps below the others on desktop.
948
+ */
949
+ get columnWidthTotal() {
950
+ const total = self.columns.filter((c) => !c.hidden).reduce((sum, c) => sum + c.width, 0);
951
+ return Math.round(total * 100) / 100;
952
+ },
953
+ /** Whether the columns overflow the section ({@link columnWidthTotal} over 100). */
954
+ get columnsOverflow() {
955
+ return this.columnWidthTotal > 100;
956
+ },
957
+ /**
958
+ * Display name for layers panel
959
+ */
960
+ get displayName() {
961
+ const count = self.columns.length;
962
+ if (count === 1) {
963
+ return "Full Width Section";
964
+ }
965
+ return `${count}-Column Section`;
966
+ }
967
+ }));
968
+ var SectionModel = SectionModelBase.preProcessSnapshot(
969
+ (snapshot) => fillDefaults(fillColumnWidths(paddingIn(snapshot)), SECTION_DEFAULTS)
970
+ ).postProcessSnapshot((snapshot) => paddingOut(dropFilled(snapshot)));
971
+ function createSection(options = {}) {
972
+ const columnCount = options.columnCount || 1;
973
+ const columnWidth = Math.floor(100 / columnCount);
974
+ const columns = [];
975
+ for (let i = 0; i < columnCount; i++) {
976
+ columns.push(createColumn({
977
+ width: i === columnCount - 1 ? 100 - columnWidth * (columnCount - 1) : columnWidth
978
+ }));
979
+ }
980
+ return {
981
+ id: options.id || nanoid3(),
982
+ type: "section",
983
+ backgroundColor: options.backgroundColor,
984
+ columns
985
+ };
986
+ }
987
+
988
+ // src/store/mst/models/WrapperModel.ts
989
+ import { types as types4 } from "mobx-state-tree";
990
+ import { nanoid as nanoid4 } from "nanoid";
991
+ var MJML_WRAPPER_DEFAULT_PADDING = { top: "20px", right: "0px", bottom: "20px", left: "0px" };
992
+ var WrapperModelBase = types4.model("Wrapper", {
993
+ id: types4.identifier,
994
+ type: types4.literal("wrapper"),
995
+ hidden: types4.optional(types4.boolean, false),
996
+ // Background
997
+ backgroundColor: types4.maybe(types4.string),
998
+ backgroundImage: types4.maybe(types4.string),
999
+ backgroundGradient: types4.maybe(types4.frozen()),
1000
+ backgroundPosition: types4.maybe(types4.string),
1001
+ backgroundRepeat: types4.maybe(types4.enumeration(["repeat", "no-repeat"])),
1002
+ backgroundSize: types4.maybe(types4.string),
1003
+ // Border
1004
+ border: types4.maybe(types4.string),
1005
+ borderTop: types4.maybe(types4.string),
1006
+ borderRight: types4.maybe(types4.string),
1007
+ borderBottom: types4.maybe(types4.string),
1008
+ borderLeft: types4.maybe(types4.string),
1009
+ borderRadius: types4.maybe(types4.string),
1010
+ // Padding (the schema's `padding` object, flat in the store; see `spacingSnapshot.ts`)
1011
+ paddingTop: types4.maybe(types4.string),
1012
+ paddingRight: types4.maybe(types4.string),
1013
+ paddingBottom: types4.maybe(types4.string),
1014
+ paddingLeft: types4.maybe(types4.string),
1015
+ // Layout
1016
+ fullWidth: types4.optional(types4.boolean, false),
1017
+ cssClass: types4.maybe(types4.string),
1018
+ gap: types4.maybe(types4.string),
1019
+ textAlign: types4.maybe(types4.enumeration(["left", "center", "right"])),
1020
+ sections: types4.array(SectionModel),
1021
+ /** MJML attributes the inspector has no control for (kept from an import). */
1022
+ extraAttributes: types4.maybe(types4.frozen()),
1023
+ /** Defaults the store filled when it opened the node; they go back out only if changed (see `filledDefaults.ts`). */
1024
+ filled: types4.maybe(types4.frozen())
1025
+ }).actions((self) => ({
1026
+ /** Set any of the inspector's fields; `undefined` clears one. */
1027
+ updateProperties(updates) {
1028
+ for (const [key, value] of Object.entries(updates)) {
1029
+ if (key in self) self[key] = value;
1030
+ }
1031
+ },
1032
+ toggleHidden() {
1033
+ self.hidden = !self.hidden;
1034
+ },
1035
+ toggleFullWidth() {
1036
+ self.fullWidth = !self.fullWidth;
1037
+ }
1038
+ })).views((self) => ({
1039
+ get isEmpty() {
1040
+ return self.sections.length === 0 || self.sections.every((s) => s.isEmpty);
1041
+ },
1042
+ get displayName() {
1043
+ const n = self.sections.length;
1044
+ return n === 0 ? "Container (empty)" : `Container, ${n} ${n === 1 ? "section" : "sections"}`;
1045
+ },
1046
+ /**
1047
+ * The padding the mail gets: MJML's `20px 0` when none is set. As soon as
1048
+ * one side is set, the compiler writes all four and an unset side is 0.
1049
+ */
1050
+ get effectivePadding() {
1051
+ const { paddingTop, paddingRight, paddingBottom, paddingLeft } = self;
1052
+ if (!paddingTop && !paddingRight && !paddingBottom && !paddingLeft) return { ...MJML_WRAPPER_DEFAULT_PADDING };
1053
+ return { top: paddingTop || "0px", right: paddingRight || "0px", bottom: paddingBottom || "0px", left: paddingLeft || "0px" };
1054
+ },
1055
+ /** The wrapper's box on the canvas, styled as MJML renders it. */
1056
+ get computedStyle() {
1057
+ const style = {};
1058
+ const gradientCSS = self.backgroundGradient ? buildGradientCSS(self.backgroundGradient) : void 0;
1059
+ if (gradientCSS) {
1060
+ style.backgroundColor = self.backgroundGradient?.stops[0]?.color;
1061
+ style.backgroundImage = gradientCSS;
1062
+ } else {
1063
+ if (self.backgroundColor) style.backgroundColor = self.backgroundColor;
1064
+ if (self.backgroundImage) {
1065
+ style.backgroundImage = `url(${self.backgroundImage})`;
1066
+ style.backgroundPosition = self.backgroundPosition || "top center";
1067
+ style.backgroundSize = self.backgroundSize || "auto";
1068
+ style.backgroundRepeat = self.backgroundRepeat || "repeat";
1069
+ }
1070
+ }
1071
+ if (self.border) style.border = self.border;
1072
+ if (self.borderTop) style.borderTop = self.borderTop;
1073
+ if (self.borderRight) style.borderRight = self.borderRight;
1074
+ if (self.borderBottom) style.borderBottom = self.borderBottom;
1075
+ if (self.borderLeft) style.borderLeft = self.borderLeft;
1076
+ if (self.borderRadius) {
1077
+ style.borderRadius = self.borderRadius;
1078
+ style.overflow = "hidden";
1079
+ }
1080
+ const p = this.effectivePadding;
1081
+ style.paddingTop = p.top;
1082
+ style.paddingRight = p.right;
1083
+ style.paddingBottom = p.bottom;
1084
+ style.paddingLeft = p.left;
1085
+ if (self.textAlign) style.textAlign = self.textAlign;
1086
+ return style;
1087
+ }
1088
+ }));
1089
+ var WrapperModel = WrapperModelBase.preProcessSnapshot(
1090
+ (snapshot) => fillDefaults(paddingIn(snapshot), WRAPPER_DEFAULTS)
1091
+ ).postProcessSnapshot((snapshot) => paddingOut(dropFilled(snapshot)));
1092
+ function createWrapper(options = {}) {
1093
+ return {
1094
+ id: options.id || nanoid4(),
1095
+ type: "wrapper",
1096
+ sections: options.sections ?? [createSection()]
1097
+ };
1098
+ }
1099
+
1100
+ // src/store/mst/models/TemplateModel.ts
1101
+ import { types as types5, destroy as destroy4, detach as detach4, getSnapshot } from "mobx-state-tree";
1102
+ import { nanoid as nanoid5 } from "nanoid";
1103
+ var TopLevelItemModel = types5.union(
1104
+ {
1105
+ dispatcher: (snapshot) => snapshot?.type === "wrapper" ? WrapperModel : SectionModel
1106
+ },
1107
+ SectionModel,
1108
+ WrapperModel
1109
+ );
1110
+ function isWrapperInstance(item) {
1111
+ return item?.type === "wrapper";
1112
+ }
1113
+ function cloneSectionSnapshot(snapshot) {
1114
+ const cloneBlocks = (blocks) => (blocks ?? []).map((b) => ({ ...b, id: nanoid5() }));
1115
+ return {
1116
+ ...snapshot,
1117
+ id: nanoid5(),
1118
+ columns: (snapshot.columns ?? []).map((col) => ({
1119
+ ...col,
1120
+ id: nanoid5(),
1121
+ blocks: cloneBlocks(col.blocks),
1122
+ ...col.subColumns ? { subColumns: col.subColumns.map((sc) => ({ ...sc, id: nanoid5(), blocks: cloneBlocks(sc.blocks) })) } : {}
1123
+ }))
1124
+ };
1125
+ }
1126
+ var FontDefinitionModel = types5.model("FontDefinition", {
1127
+ name: types5.string,
1128
+ href: types5.string
1129
+ });
1130
+ var ThemeColorModel = types5.model("ThemeColor", {
1131
+ name: types5.string,
1132
+ value: types5.string
1133
+ }).actions((self) => ({
1134
+ setValue(value) {
1135
+ self.value = value;
1136
+ },
1137
+ setName(name) {
1138
+ self.name = name;
1139
+ }
1140
+ }));
1141
+ var DEFAULT_THEME_COLORS = [
1142
+ { name: "Primary", value: "#944923" },
1143
+ { name: "Secondary", value: "#ffffff" },
1144
+ { name: "Text", value: "#333333" },
1145
+ { name: "Background", value: "#f5f5f5" }
1146
+ ];
1147
+ var METADATA_DEFAULTS = {
1148
+ title: () => "Untitled Template",
1149
+ subject: () => "",
1150
+ previewText: () => "",
1151
+ createdAt: () => Date.now(),
1152
+ updatedAt: () => Date.now(),
1153
+ fonts: () => [],
1154
+ themeColors: () => DEFAULT_THEME_COLORS.map((c) => ({ ...c }))
1155
+ };
1156
+ var TemplateMetadataModel = types5.model("TemplateMetadata", {
1157
+ title: types5.optional(types5.string, "Untitled Template"),
1158
+ subject: types5.optional(types5.string, ""),
1159
+ previewText: types5.optional(types5.string, ""),
1160
+ createdAt: types5.optional(types5.Date, () => /* @__PURE__ */ new Date()),
1161
+ updatedAt: types5.optional(types5.Date, () => /* @__PURE__ */ new Date()),
1162
+ fonts: types5.optional(types5.array(FontDefinitionModel), []),
1163
+ themeColors: types5.optional(types5.array(ThemeColorModel), DEFAULT_THEME_COLORS),
1164
+ breakpoint: types5.maybe(types5.string),
1165
+ customCSS: types5.maybe(types5.string),
1166
+ inlineCSS: types5.maybe(types5.string),
1167
+ /** The MJML head and body settings of an imported document (see `MjmlHead` in the schema). */
1168
+ mjmlHead: types5.maybe(types5.frozen()),
1169
+ /** Defaults the store filled when it opened the node; they go back out only if changed (see `filledDefaults.ts`). */
1170
+ filled: types5.maybe(types5.frozen()),
1171
+ /** Dates a stored document wrote as ISO strings, so they go back out as written. */
1172
+ dateStrings: types5.maybe(types5.frozen())
1173
+ }).actions((self) => ({
1174
+ update(updates) {
1175
+ Object.entries(updates).forEach(([key, value]) => {
1176
+ if (key in self && value !== void 0) {
1177
+ self[key] = value;
1178
+ }
1179
+ });
1180
+ self.updatedAt = /* @__PURE__ */ new Date();
1181
+ },
1182
+ addFont(name, href) {
1183
+ self.fonts.push(FontDefinitionModel.create({ name, href }));
1184
+ self.updatedAt = /* @__PURE__ */ new Date();
1185
+ },
1186
+ removeFont(name) {
1187
+ const index = self.fonts.findIndex((f) => f.name === name);
1188
+ if (index !== -1) {
1189
+ self.fonts.splice(index, 1);
1190
+ self.updatedAt = /* @__PURE__ */ new Date();
1191
+ }
1192
+ },
1193
+ addThemeColor(name, value) {
1194
+ self.themeColors.push(ThemeColorModel.create({ name, value }));
1195
+ self.updatedAt = /* @__PURE__ */ new Date();
1196
+ },
1197
+ updateThemeColor(name, value) {
1198
+ const color = self.themeColors.find((c) => c.name === name);
1199
+ if (color) {
1200
+ color.setValue(value);
1201
+ self.updatedAt = /* @__PURE__ */ new Date();
1202
+ }
1203
+ },
1204
+ removeThemeColor(name) {
1205
+ const index = self.themeColors.findIndex((c) => c.name === name);
1206
+ if (index !== -1) {
1207
+ self.themeColors.splice(index, 1);
1208
+ self.updatedAt = /* @__PURE__ */ new Date();
1209
+ }
1210
+ },
1211
+ touch() {
1212
+ self.updatedAt = /* @__PURE__ */ new Date();
1213
+ }
1214
+ })).preProcessSnapshot((snapshot) => {
1215
+ if (!snapshot) return snapshot;
1216
+ const dateStrings = {};
1217
+ const toDate = (key, value) => {
1218
+ if (typeof value !== "string") return value;
1219
+ const ms = Date.parse(value);
1220
+ if (Number.isNaN(ms)) return void 0;
1221
+ dateStrings[key] = value;
1222
+ return ms;
1223
+ };
1224
+ const parsed = {
1225
+ ...snapshot,
1226
+ createdAt: toDate("createdAt", snapshot.createdAt),
1227
+ updatedAt: toDate("updatedAt", snapshot.updatedAt),
1228
+ ...Object.keys(dateStrings).length > 0 ? { dateStrings } : {}
1229
+ };
1230
+ return fillDefaults(parsed, METADATA_DEFAULTS);
1231
+ }).postProcessSnapshot((snapshot) => {
1232
+ const { dateStrings, ...rest } = dropFilled(snapshot);
1233
+ const out = rest;
1234
+ for (const [key, written] of Object.entries(dateStrings ?? {})) {
1235
+ if (out[key] === Date.parse(written)) out[key] = written;
1236
+ }
1237
+ return out;
1238
+ });
1239
+ var TemplateModel = types5.model("Template", {
1240
+ id: types5.optional(types5.string, () => nanoid5()),
1241
+ version: types5.optional(types5.string, CURRENT_TEMPLATE_VERSION),
1242
+ metadata: types5.optional(TemplateMetadataModel, {}),
1243
+ /** The top level, in order: sections and wrappers (see `TopLevelItemModel`). */
1244
+ sections: types5.array(TopLevelItemModel)
1245
+ }).preProcessSnapshot((snapshot) => {
1246
+ if (!snapshot || snapshot.version !== "1.0" || !Array.isArray(snapshot.sections)) return snapshot;
1247
+ return migrateV1_0ToV1_1(snapshot);
1248
+ }).views((self) => ({
1249
+ /** Every section in document order, the ones inside wrappers included. */
1250
+ get allSections() {
1251
+ const out = [];
1252
+ for (const item of self.sections) {
1253
+ if (isWrapperInstance(item)) out.push(...item.sections);
1254
+ else out.push(item);
1255
+ }
1256
+ return out;
1257
+ },
1258
+ /** The top-level wrappers, in order. */
1259
+ get wrappers() {
1260
+ return self.sections.filter(isWrapperInstance);
1261
+ }
1262
+ })).views((self) => ({
1263
+ getSectionById(sectionId) {
1264
+ return self.allSections.find((s) => s.id === sectionId);
1265
+ },
1266
+ getWrapperById(wrapperId) {
1267
+ return self.wrappers.find((w) => w.id === wrapperId);
1268
+ },
1269
+ /** The wrapper a section sits in; undefined for a top-level section (or an unknown id). */
1270
+ findWrapperBySectionId(sectionId) {
1271
+ return self.wrappers.find((w) => w.sections.some((s) => s.id === sectionId));
1272
+ },
1273
+ /** The index of a top-level item (section or wrapper); -1 when it is not at the top level. */
1274
+ getSectionIndex(itemId) {
1275
+ return self.sections.findIndex((s) => s.id === itemId);
1276
+ }
1277
+ })).actions((self) => {
1278
+ const locate = (sectionId) => {
1279
+ const top = self.sections.findIndex((s) => s.id === sectionId && !isWrapperInstance(s));
1280
+ if (top !== -1) return { list: self.sections, index: top };
1281
+ for (const wrapper of self.wrappers) {
1282
+ const index = wrapper.sections.findIndex((s) => s.id === sectionId);
1283
+ if (index !== -1) return { list: wrapper.sections, index, wrapper };
1284
+ }
1285
+ return void 0;
1286
+ };
1287
+ return {
1288
+ /**
1289
+ * Add a section at the top level, or into a wrapper when `wrapperId`
1290
+ * names one. `index` is the position in that list; the end when left out.
1291
+ */
1292
+ addSection(section, index, wrapperId) {
1293
+ const sectionToAdd = SectionModel.create(section);
1294
+ const wrapper = wrapperId ? self.getWrapperById(wrapperId) : void 0;
1295
+ if (wrapperId && !wrapper) throw new Error(`No wrapper with id "${wrapperId}"`);
1296
+ const list = wrapper ? wrapper.sections : self.sections;
1297
+ if (index !== void 0 && index >= 0 && index <= list.length) list.splice(index, 0, sectionToAdd);
1298
+ else list.push(sectionToAdd);
1299
+ self.metadata.touch();
1300
+ return sectionToAdd;
1301
+ },
1302
+ /** Add a wrapper at the top level (by default around one empty section). */
1303
+ addWrapper(wrapper = createWrapper(), index) {
1304
+ const wrapperToAdd = WrapperModel.create(wrapper);
1305
+ if (index !== void 0 && index >= 0 && index <= self.sections.length) self.sections.splice(index, 0, wrapperToAdd);
1306
+ else self.sections.push(wrapperToAdd);
1307
+ self.metadata.touch();
1308
+ return wrapperToAdd;
1309
+ },
1310
+ /** Remove a section, wherever it is (a wrapper that loses its last section stays, empty). */
1311
+ removeSection(sectionId) {
1312
+ const at = locate(sectionId);
1313
+ if (!at) return false;
1314
+ destroy4(at.list[at.index]);
1315
+ self.metadata.touch();
1316
+ return true;
1317
+ },
1318
+ /**
1319
+ * Move a top-level item (a section or a wrapper) to another position
1320
+ * of the top level.
1321
+ */
1322
+ moveSection(itemId, toIndex) {
1323
+ const fromIndex = self.sections.findIndex((s) => s.id === itemId);
1324
+ if (fromIndex === -1 || toIndex < 0 || toIndex >= self.sections.length) return false;
1325
+ if (fromIndex === toIndex) return false;
1326
+ const item = detach4(self.sections[fromIndex]);
1327
+ self.sections.splice(toIndex, 0, item);
1328
+ self.metadata.touch();
1329
+ return true;
1330
+ },
1331
+ /**
1332
+ * Move a section into a wrapper, out of one, between two, or within one.
1333
+ * `wrapperId: null` targets the top level. `index` is the position in
1334
+ * the target list as it is after the section left its old place.
1335
+ */
1336
+ moveSectionTo(sectionId, target) {
1337
+ const at = locate(sectionId);
1338
+ if (!at) return false;
1339
+ const wrapper = target.wrapperId ? self.getWrapperById(target.wrapperId) : void 0;
1340
+ if (target.wrapperId && !wrapper) return false;
1341
+ const sameList = (at.wrapper?.id ?? null) === (target.wrapperId ?? null);
1342
+ if (sameList && at.index === target.index) return false;
1343
+ const section = detach4(at.list[at.index]);
1344
+ const list = wrapper ? wrapper.sections : self.sections;
1345
+ const index = Math.max(0, Math.min(target.index, list.length));
1346
+ list.splice(index, 0, section);
1347
+ self.metadata.touch();
1348
+ return true;
1349
+ },
1350
+ /** Put a top-level section into a new wrapper, in its place. Returns the wrapper. */
1351
+ wrapSection(sectionId) {
1352
+ const index = self.sections.findIndex((s) => s.id === sectionId && !isWrapperInstance(s));
1353
+ if (index === -1) return void 0;
1354
+ const section = detach4(self.sections[index]);
1355
+ const wrapper = WrapperModel.create({ id: nanoid5(), type: "wrapper", sections: [] });
1356
+ self.sections.splice(index, 0, wrapper);
1357
+ wrapper.sections.push(section);
1358
+ self.metadata.touch();
1359
+ return wrapper;
1360
+ },
1361
+ /** Take a wrapper's sections out to the top level, in its place, and remove the wrapper. Returns the sections' ids. */
1362
+ unwrap(wrapperId) {
1363
+ const index = self.sections.findIndex((s) => s.id === wrapperId && isWrapperInstance(s));
1364
+ if (index === -1) return [];
1365
+ const wrapper = self.sections[index];
1366
+ const sections = wrapper.sections.slice().map((s) => detach4(s));
1367
+ destroy4(wrapper);
1368
+ self.sections.splice(index, 0, ...sections);
1369
+ self.metadata.touch();
1370
+ return sections.map((s) => s.id);
1371
+ },
1372
+ /**
1373
+ * Remove a wrapper. With `keepSections`, its sections stay, in its place
1374
+ * (the same as {@link unwrap}); otherwise they go with it.
1375
+ */
1376
+ removeWrapper(wrapperId, options) {
1377
+ const wrapper = self.getWrapperById(wrapperId);
1378
+ if (!wrapper) return false;
1379
+ if (options.keepSections) {
1380
+ this.unwrap(wrapperId);
1381
+ } else {
1382
+ destroy4(wrapper);
1383
+ self.metadata.touch();
1384
+ }
1385
+ return true;
1386
+ },
1387
+ /** Duplicate a section right after itself, in the same wrapper (or at the top level). */
1388
+ duplicateSection(sectionId) {
1389
+ const at = locate(sectionId);
1390
+ if (!at) return void 0;
1391
+ const newSection = SectionModel.create(cloneSectionSnapshot(getSnapshot(at.list[at.index])));
1392
+ at.list.splice(at.index + 1, 0, newSection);
1393
+ self.metadata.touch();
1394
+ return newSection;
1395
+ },
1396
+ /** Duplicate a wrapper and everything inside it, right after itself. */
1397
+ duplicateWrapper(wrapperId) {
1398
+ const index = self.sections.findIndex((s) => s.id === wrapperId && isWrapperInstance(s));
1399
+ if (index === -1) return void 0;
1400
+ const snapshot = getSnapshot(self.sections[index]);
1401
+ const copy = WrapperModel.create({
1402
+ ...snapshot,
1403
+ id: nanoid5(),
1404
+ sections: snapshot.sections.map((s) => cloneSectionSnapshot(s))
1405
+ });
1406
+ self.sections.splice(index + 1, 0, copy);
1407
+ self.metadata.touch();
1408
+ return copy;
1409
+ },
1410
+ insertBlock(columnId, block, index) {
1411
+ for (const section of self.allSections) {
1412
+ const column = section.getColumnById(columnId);
1413
+ if (column) {
1414
+ self.metadata.touch();
1415
+ const newBlock = BlockModel.create(block);
1416
+ if (index !== void 0 && index >= 0 && index <= column.blocks.length) {
1417
+ column.blocks.splice(index, 0, newBlock);
1418
+ } else {
1419
+ column.blocks.push(newBlock);
1420
+ }
1421
+ return newBlock;
1422
+ }
1423
+ }
1424
+ return void 0;
1425
+ },
1426
+ moveBlock(blockId, targetColumnId, targetIndex) {
1427
+ let sourceColumn;
1428
+ let block;
1429
+ for (const section of self.allSections) {
1430
+ const col = section.findColumnByBlockId(blockId);
1431
+ if (col) {
1432
+ sourceColumn = col;
1433
+ block = col.getBlockById(blockId);
1434
+ break;
1435
+ }
1436
+ }
1437
+ if (!sourceColumn || !block) return false;
1438
+ let targetColumn;
1439
+ for (const section of self.allSections) {
1440
+ const col = section.getColumnById(targetColumnId);
1441
+ if (col) {
1442
+ targetColumn = col;
1443
+ break;
1444
+ }
1445
+ }
1446
+ if (!targetColumn) return false;
1447
+ if (sourceColumn.id === targetColumn.id) {
1448
+ const fromIndex = sourceColumn.getBlockIndex(blockId);
1449
+ sourceColumn.moveBlock(fromIndex, targetIndex);
1450
+ } else {
1451
+ const detachedBlock = sourceColumn.detachBlock(blockId);
1452
+ if (detachedBlock) {
1453
+ if (targetIndex >= 0 && targetIndex <= targetColumn.blocks.length) {
1454
+ targetColumn.blocks.splice(targetIndex, 0, detachedBlock);
1455
+ } else {
1456
+ targetColumn.blocks.push(detachedBlock);
1457
+ }
1458
+ }
1459
+ }
1460
+ self.metadata.touch();
1461
+ return true;
1462
+ },
1463
+ deleteBlock(blockId) {
1464
+ for (const section of self.allSections) {
1465
+ for (const column of section.columns) {
1466
+ if (column.removeBlock(blockId)) {
1467
+ self.metadata.touch();
1468
+ return true;
1469
+ }
1470
+ }
1471
+ }
1472
+ return false;
1473
+ },
1474
+ updateMetadata(updates) {
1475
+ self.metadata.update(updates);
1476
+ },
1477
+ clear() {
1478
+ self.sections.forEach((s) => destroy4(s));
1479
+ self.sections.clear();
1480
+ self.metadata.touch();
1481
+ }
1482
+ };
1483
+ }).views((self) => ({
1484
+ findColumnById(columnId) {
1485
+ for (const section of self.allSections) {
1486
+ const column = section.getColumnById(columnId);
1487
+ if (column) return column;
1488
+ }
1489
+ return void 0;
1490
+ },
1491
+ findBlockById(blockId) {
1492
+ for (const section of self.allSections) {
1493
+ const block = section.findBlockById(blockId);
1494
+ if (block) return block;
1495
+ }
1496
+ return void 0;
1497
+ },
1498
+ findSectionByBlockId(blockId) {
1499
+ for (const section of self.allSections) {
1500
+ if (section.findBlockById(blockId)) {
1501
+ return section;
1502
+ }
1503
+ }
1504
+ return void 0;
1505
+ },
1506
+ findColumnByBlockId(blockId) {
1507
+ for (const section of self.allSections) {
1508
+ const column = section.findColumnByBlockId(blockId);
1509
+ if (column) return column;
1510
+ }
1511
+ return void 0;
1512
+ },
1513
+ /** Visible top-level items (a hidden wrapper hides everything inside it). */
1514
+ get visibleSections() {
1515
+ return self.sections.filter((s) => !s.hidden);
1516
+ },
1517
+ /** How many sections the document has, the ones inside wrappers included. */
1518
+ get sectionCount() {
1519
+ return self.allSections.length;
1520
+ },
1521
+ get totalBlockCount() {
1522
+ return self.allSections.reduce((sum, section) => sum + section.totalBlockCount, 0);
1523
+ },
1524
+ get isEmpty() {
1525
+ return self.allSections.every((s) => s.isEmpty);
1526
+ },
1527
+ getBlocksByType(type) {
1528
+ const blocks = [];
1529
+ for (const section of self.allSections) {
1530
+ for (const column of section.columns) {
1531
+ for (const block of column.blocks) {
1532
+ if (block.type === type) {
1533
+ blocks.push(block);
1534
+ }
1535
+ }
1536
+ }
1537
+ }
1538
+ return blocks;
1539
+ },
1540
+ get allBlocks() {
1541
+ const blocks = [];
1542
+ for (const section of self.allSections) {
1543
+ for (const column of section.columns) {
1544
+ blocks.push(...column.blocks);
1545
+ }
1546
+ }
1547
+ return blocks;
1548
+ }
1549
+ }));
1550
+ function createTemplate(options = {}) {
1551
+ return {
1552
+ id: options.id || nanoid5(),
1553
+ version: CURRENT_TEMPLATE_VERSION,
1554
+ metadata: {
1555
+ title: options.title || "Untitled Template",
1556
+ createdAt: /* @__PURE__ */ new Date(),
1557
+ updatedAt: /* @__PURE__ */ new Date()
1558
+ },
1559
+ sections: options.sections || []
1560
+ };
1561
+ }
1562
+ function createTemplateWithDefaultSection(options = {}) {
1563
+ return {
1564
+ id: options.id || nanoid5(),
1565
+ version: CURRENT_TEMPLATE_VERSION,
1566
+ metadata: {
1567
+ title: options.title || "Untitled Template",
1568
+ createdAt: /* @__PURE__ */ new Date(),
1569
+ updatedAt: /* @__PURE__ */ new Date()
1570
+ },
1571
+ sections: [createSection()]
1572
+ };
1573
+ }
1574
+
1575
+ // src/store/mst/EditorUIStore.ts
1576
+ import { types as types6 } from "mobx-state-tree";
1577
+ var EditorUIStore = types6.model("EditorUI", {
1578
+ // === Selection State ===
1579
+ selectedBlockId: types6.maybe(types6.string),
1580
+ selectedSectionId: types6.maybe(types6.string),
1581
+ selectedColumnId: types6.maybe(types6.string),
1582
+ selectedSubColumnId: types6.maybe(types6.string),
1583
+ /** A wrapper (container around sections), one level out from sections. */
1584
+ selectedWrapperId: types6.maybe(types6.string),
1585
+ // === Panel State ===
1586
+ activeTab: types6.optional(
1587
+ types6.enumeration(["elements", "layout", "layers", "settings", "prebuilt", "saved"]),
1588
+ "elements"
1589
+ ),
1590
+ showLeftPanel: types6.optional(types6.boolean, true),
1591
+ showRightPanel: types6.optional(types6.boolean, true),
1592
+ // === View Mode ===
1593
+ previewDevice: types6.optional(
1594
+ types6.enumeration(["desktop", "mobile"]),
1595
+ "desktop"
1596
+ ),
1597
+ zoomLevel: types6.optional(types6.number, 1)
1598
+ }).volatile(() => ({
1599
+ // === Transient State (not persisted) ===
1600
+ hoverBlockId: void 0,
1601
+ hoverSectionId: void 0,
1602
+ hoverColumnId: void 0,
1603
+ hoverSubColumnId: void 0,
1604
+ hoverWrapperId: void 0,
1605
+ /** The wrapper whose delete the editor is asking about (keep its sections, or delete everything). */
1606
+ pendingWrapperDeleteId: void 0,
1607
+ // Drag state
1608
+ isDragging: false,
1609
+ dragData: void 0,
1610
+ dropIntent: void 0,
1611
+ // Resize state
1612
+ isResizing: false,
1613
+ resizeState: void 0,
1614
+ // Inline editing state
1615
+ isInlineEditing: false,
1616
+ inlineEditBlockId: void 0
1617
+ })).actions((self) => ({
1618
+ // === Selection Actions ===
1619
+ /**
1620
+ * Select a block
1621
+ */
1622
+ selectBlock(blockId) {
1623
+ self.selectedBlockId = blockId || void 0;
1624
+ self.selectedSectionId = void 0;
1625
+ self.selectedColumnId = void 0;
1626
+ self.selectedSubColumnId = void 0;
1627
+ self.selectedWrapperId = void 0;
1628
+ },
1629
+ /**
1630
+ * Select a section
1631
+ */
1632
+ selectSection(sectionId) {
1633
+ self.selectedSectionId = sectionId || void 0;
1634
+ self.selectedBlockId = void 0;
1635
+ self.selectedColumnId = void 0;
1636
+ self.selectedSubColumnId = void 0;
1637
+ self.selectedWrapperId = void 0;
1638
+ },
1639
+ /**
1640
+ * Select a column
1641
+ */
1642
+ selectColumn(columnId) {
1643
+ self.selectedColumnId = columnId || void 0;
1644
+ self.selectedBlockId = void 0;
1645
+ self.selectedSectionId = void 0;
1646
+ self.selectedSubColumnId = void 0;
1647
+ self.selectedWrapperId = void 0;
1648
+ },
1649
+ /**
1650
+ * Select a sub-column (depth-2 nested column).
1651
+ * Mutually exclusive with the other selection levels.
1652
+ */
1653
+ selectSubColumn(subColumnId) {
1654
+ self.selectedSubColumnId = subColumnId || void 0;
1655
+ self.selectedBlockId = void 0;
1656
+ self.selectedSectionId = void 0;
1657
+ self.selectedColumnId = void 0;
1658
+ self.selectedWrapperId = void 0;
1659
+ },
1660
+ /**
1661
+ * Select a wrapper (the container around sections).
1662
+ * Mutually exclusive with the other selection levels.
1663
+ */
1664
+ selectWrapper(wrapperId) {
1665
+ self.selectedWrapperId = wrapperId || void 0;
1666
+ self.selectedBlockId = void 0;
1667
+ self.selectedSectionId = void 0;
1668
+ self.selectedColumnId = void 0;
1669
+ self.selectedSubColumnId = void 0;
1670
+ },
1671
+ /**
1672
+ * Clear all selection
1673
+ */
1674
+ clearSelection() {
1675
+ self.selectedBlockId = void 0;
1676
+ self.selectedSectionId = void 0;
1677
+ self.selectedColumnId = void 0;
1678
+ self.selectedSubColumnId = void 0;
1679
+ self.selectedWrapperId = void 0;
1680
+ },
1681
+ // === Hover Actions ===
1682
+ /**
1683
+ * Set hover block
1684
+ */
1685
+ setHoverBlock(blockId) {
1686
+ self.hoverBlockId = blockId;
1687
+ },
1688
+ /**
1689
+ * Set hover section
1690
+ */
1691
+ setHoverSection(sectionId) {
1692
+ self.hoverSectionId = sectionId;
1693
+ },
1694
+ /**
1695
+ * Set hover column
1696
+ */
1697
+ setHoverColumn(columnId) {
1698
+ self.hoverColumnId = columnId;
1699
+ },
1700
+ /**
1701
+ * Set hover sub-column (depth-2 nested)
1702
+ */
1703
+ setHoverSubColumn(subColumnId) {
1704
+ self.hoverSubColumnId = subColumnId;
1705
+ },
1706
+ /**
1707
+ * Set hover wrapper
1708
+ */
1709
+ setHoverWrapper(wrapperId) {
1710
+ self.hoverWrapperId = wrapperId;
1711
+ },
1712
+ // === Wrapper delete dialog ===
1713
+ /** Ask whether to keep a wrapper's sections or delete everything (the editor shows its own dialog). */
1714
+ requestWrapperDelete(wrapperId) {
1715
+ self.pendingWrapperDeleteId = wrapperId;
1716
+ },
1717
+ cancelWrapperDelete() {
1718
+ self.pendingWrapperDeleteId = void 0;
1719
+ },
1720
+ /**
1721
+ * Clear all hover states
1722
+ */
1723
+ clearHover() {
1724
+ self.hoverBlockId = void 0;
1725
+ self.hoverSectionId = void 0;
1726
+ self.hoverColumnId = void 0;
1727
+ self.hoverSubColumnId = void 0;
1728
+ self.hoverWrapperId = void 0;
1729
+ },
1730
+ // === Drag Actions ===
1731
+ /**
1732
+ * Start a drag operation for an existing block
1733
+ */
1734
+ startBlockDrag(blockId, sourceColumnId, sourceIndex) {
1735
+ self.isDragging = true;
1736
+ self.dragData = {
1737
+ blockId,
1738
+ sourceColumnId,
1739
+ sourceIndex,
1740
+ isNewBlock: false
1741
+ };
1742
+ },
1743
+ /**
1744
+ * Start a drag operation for a new block from the toolbar
1745
+ */
1746
+ startNewBlockDrag(blockType) {
1747
+ self.isDragging = true;
1748
+ self.dragData = {
1749
+ blockType,
1750
+ isNewBlock: true
1751
+ };
1752
+ },
1753
+ /**
1754
+ * Update the drop intent during drag
1755
+ */
1756
+ setDropIntent(intent) {
1757
+ self.dropIntent = intent;
1758
+ },
1759
+ /**
1760
+ * End the drag operation
1761
+ */
1762
+ endDrag() {
1763
+ self.isDragging = false;
1764
+ self.dragData = void 0;
1765
+ self.dropIntent = void 0;
1766
+ },
1767
+ // === Resize Actions ===
1768
+ /**
1769
+ * Start a resize operation
1770
+ */
1771
+ startResize(targetId, handleType, originalValue) {
1772
+ self.isResizing = true;
1773
+ self.resizeState = {
1774
+ targetId,
1775
+ handleType,
1776
+ originalValue,
1777
+ previewValue: originalValue
1778
+ };
1779
+ },
1780
+ /**
1781
+ * Update resize preview value
1782
+ */
1783
+ updateResizePreview(value) {
1784
+ if (self.resizeState) {
1785
+ self.resizeState.previewValue = value;
1786
+ }
1787
+ },
1788
+ /**
1789
+ * End resize operation (commit or cancel)
1790
+ */
1791
+ endResize() {
1792
+ self.isResizing = false;
1793
+ self.resizeState = void 0;
1794
+ },
1795
+ // === Inline Editing Actions ===
1796
+ /**
1797
+ * Start inline editing for a block
1798
+ */
1799
+ startInlineEdit(blockId) {
1800
+ self.isInlineEditing = true;
1801
+ self.inlineEditBlockId = blockId;
1802
+ self.selectedBlockId = blockId;
1803
+ },
1804
+ /**
1805
+ * End inline editing
1806
+ */
1807
+ endInlineEdit() {
1808
+ self.isInlineEditing = false;
1809
+ self.inlineEditBlockId = void 0;
1810
+ },
1811
+ // === Panel Actions ===
1812
+ /**
1813
+ * Set the active sidebar tab
1814
+ */
1815
+ setActiveTab(tab) {
1816
+ self.activeTab = tab;
1817
+ },
1818
+ /**
1819
+ * Toggle left panel visibility
1820
+ */
1821
+ toggleLeftPanel() {
1822
+ self.showLeftPanel = !self.showLeftPanel;
1823
+ },
1824
+ /**
1825
+ * Toggle right panel visibility
1826
+ */
1827
+ toggleRightPanel() {
1828
+ self.showRightPanel = !self.showRightPanel;
1829
+ },
1830
+ /**
1831
+ * Set left panel visibility
1832
+ */
1833
+ setLeftPanelVisible(visible) {
1834
+ self.showLeftPanel = visible;
1835
+ },
1836
+ /**
1837
+ * Set right panel visibility
1838
+ */
1839
+ setRightPanelVisible(visible) {
1840
+ self.showRightPanel = visible;
1841
+ },
1842
+ // === View Actions ===
1843
+ /**
1844
+ * Set preview device
1845
+ */
1846
+ setPreviewDevice(device) {
1847
+ self.previewDevice = device;
1848
+ },
1849
+ /**
1850
+ * Toggle preview device between desktop and mobile
1851
+ */
1852
+ togglePreviewDevice() {
1853
+ self.previewDevice = self.previewDevice === "desktop" ? "mobile" : "desktop";
1854
+ },
1855
+ /**
1856
+ * Set zoom level
1857
+ */
1858
+ setZoomLevel(zoom) {
1859
+ self.zoomLevel = Math.max(0.25, Math.min(2, zoom));
1860
+ },
1861
+ /**
1862
+ * Zoom in
1863
+ */
1864
+ zoomIn() {
1865
+ self.zoomLevel = Math.min(2, self.zoomLevel + 0.1);
1866
+ },
1867
+ /**
1868
+ * Zoom out
1869
+ */
1870
+ zoomOut() {
1871
+ self.zoomLevel = Math.max(0.25, self.zoomLevel - 0.1);
1872
+ },
1873
+ /**
1874
+ * Reset zoom to 100%
1875
+ */
1876
+ resetZoom() {
1877
+ self.zoomLevel = 1;
1878
+ }
1879
+ })).views((self) => ({
1880
+ /**
1881
+ * Get the current selection type
1882
+ */
1883
+ get selectionType() {
1884
+ if (self.selectedBlockId) return "block";
1885
+ if (self.selectedSectionId) return "section";
1886
+ if (self.selectedColumnId) return "column";
1887
+ if (self.selectedSubColumnId) return "subColumn";
1888
+ if (self.selectedWrapperId) return "wrapper";
1889
+ return null;
1890
+ },
1891
+ /**
1892
+ * Check if anything is selected
1893
+ */
1894
+ get hasSelection() {
1895
+ return !!(self.selectedBlockId || self.selectedSectionId || self.selectedColumnId || self.selectedSubColumnId || self.selectedWrapperId);
1896
+ },
1897
+ /**
1898
+ * Check if a specific block is selected
1899
+ */
1900
+ isBlockSelected(blockId) {
1901
+ return self.selectedBlockId === blockId;
1902
+ },
1903
+ /**
1904
+ * Check if a specific section is selected
1905
+ */
1906
+ isSectionSelected(sectionId) {
1907
+ return self.selectedSectionId === sectionId;
1908
+ },
1909
+ /**
1910
+ * Check if a specific column is selected
1911
+ */
1912
+ isColumnSelected(columnId) {
1913
+ return self.selectedColumnId === columnId;
1914
+ },
1915
+ /**
1916
+ * Check if a specific wrapper is selected
1917
+ */
1918
+ isWrapperSelected(wrapperId) {
1919
+ return self.selectedWrapperId === wrapperId;
1920
+ },
1921
+ /**
1922
+ * Check if a specific block is hovered
1923
+ */
1924
+ isBlockHovered(blockId) {
1925
+ return self.hoverBlockId === blockId;
1926
+ },
1927
+ /**
1928
+ * Check if we're dragging an existing block
1929
+ */
1930
+ get isDraggingExistingBlock() {
1931
+ return self.isDragging && !!self.dragData && !self.dragData.isNewBlock;
1932
+ },
1933
+ /**
1934
+ * Check if we're dragging a new block from toolbar
1935
+ */
1936
+ get isDraggingNewBlock() {
1937
+ return self.isDragging && !!self.dragData && !!self.dragData.isNewBlock;
1938
+ },
1939
+ /**
1940
+ * Get the currently dragged block type (for new blocks)
1941
+ */
1942
+ get draggedBlockType() {
1943
+ return self.dragData?.blockType;
1944
+ },
1945
+ /**
1946
+ * Get the currently dragged block ID (for existing blocks)
1947
+ */
1948
+ get draggedBlockId() {
1949
+ return self.dragData?.blockId;
1950
+ },
1951
+ /**
1952
+ * Get preview width based on device
1953
+ */
1954
+ get previewWidth() {
1955
+ return self.previewDevice === "desktop" ? 600 : 375;
1956
+ },
1957
+ /**
1958
+ * Check if in mobile preview mode
1959
+ */
1960
+ get isMobilePreview() {
1961
+ return self.previewDevice === "mobile";
1962
+ },
1963
+ /**
1964
+ * Check if in desktop preview mode
1965
+ */
1966
+ get isDesktopPreview() {
1967
+ return self.previewDevice === "desktop";
1968
+ },
1969
+ /**
1970
+ * Get zoom percentage
1971
+ */
1972
+ get zoomPercentage() {
1973
+ return Math.round(self.zoomLevel * 100);
1974
+ }
1975
+ }));
1976
+
1977
+ // src/store/mst/RootStore.ts
1978
+ import { types as types7, onSnapshot, applySnapshot, getSnapshot as getSnapshot2 } from "mobx-state-tree";
1979
+ var RootStore = types7.model("RootStore", {
1980
+ template: TemplateModel,
1981
+ editorUI: EditorUIStore
1982
+ }).volatile(() => ({
1983
+ // History state for undo/redo. Typed as `unknown` to keep the inferred
1984
+ // RootStore type small enough for TypeScript's serialization limit;
1985
+ // history snapshots are roundtripped through MST validation anyway.
1986
+ history: [],
1987
+ historyIndex: -1,
1988
+ maxHistory: 50,
1989
+ isUndoRedo: false,
1990
+ /**
1991
+ * The snapshot undo or redo just applied. The snapshot listener runs after
1992
+ * the outermost action (undo itself) has finished, so it must recognise
1993
+ * this one and not record it as a new step: that would cut off the redo
1994
+ * future.
1995
+ */
1996
+ appliedFromHistory: void 0
1997
+ })).actions((self) => ({
1998
+ /**
1999
+ * Initialize history with current state
2000
+ */
2001
+ initHistory() {
2002
+ const snapshot = getSnapshot2(self.template);
2003
+ self.history = [snapshot];
2004
+ self.historyIndex = 0;
2005
+ },
2006
+ /**
2007
+ * Record current state to history (called after mutations)
2008
+ */
2009
+ recordHistory() {
2010
+ if (self.isUndoRedo) return;
2011
+ const snapshot = getSnapshot2(self.template);
2012
+ if (self.appliedFromHistory !== void 0) {
2013
+ const applied = self.appliedFromHistory;
2014
+ self.appliedFromHistory = void 0;
2015
+ if (JSON.stringify(applied) === JSON.stringify(snapshot)) return;
2016
+ }
2017
+ if (self.historyIndex < self.history.length - 1) {
2018
+ self.history = self.history.slice(0, self.historyIndex + 1);
2019
+ }
2020
+ self.history.push(snapshot);
2021
+ if (self.history.length > self.maxHistory) {
2022
+ self.history = self.history.slice(self.history.length - self.maxHistory);
2023
+ }
2024
+ self.historyIndex = self.history.length - 1;
2025
+ },
2026
+ /**
2027
+ * Undo to previous state
2028
+ */
2029
+ undo() {
2030
+ if (self.historyIndex <= 0) return false;
2031
+ self.isUndoRedo = true;
2032
+ self.historyIndex--;
2033
+ applySnapshot(self.template, self.history[self.historyIndex]);
2034
+ self.appliedFromHistory = getSnapshot2(self.template);
2035
+ self.isUndoRedo = false;
2036
+ this.validateSelection();
2037
+ return true;
2038
+ },
2039
+ /**
2040
+ * Redo to next state
2041
+ */
2042
+ redo() {
2043
+ if (self.historyIndex >= self.history.length - 1) return false;
2044
+ self.isUndoRedo = true;
2045
+ self.historyIndex++;
2046
+ applySnapshot(self.template, self.history[self.historyIndex]);
2047
+ self.appliedFromHistory = getSnapshot2(self.template);
2048
+ self.isUndoRedo = false;
2049
+ this.validateSelection();
2050
+ return true;
2051
+ },
2052
+ /**
2053
+ * Clear history
2054
+ */
2055
+ clearHistory() {
2056
+ const snapshot = getSnapshot2(self.template);
2057
+ self.history = [snapshot];
2058
+ self.historyIndex = 0;
2059
+ },
2060
+ /**
2061
+ * Validate and clear selection if items no longer exist
2062
+ */
2063
+ validateSelection() {
2064
+ if (self.editorUI.selectedBlockId && !self.template.findBlockById(self.editorUI.selectedBlockId)) {
2065
+ self.editorUI.clearSelection();
2066
+ }
2067
+ if (self.editorUI.selectedSectionId && !self.template.getSectionById(self.editorUI.selectedSectionId)) {
2068
+ self.editorUI.clearSelection();
2069
+ }
2070
+ if (self.editorUI.selectedColumnId && !self.template.findColumnById(self.editorUI.selectedColumnId)) {
2071
+ self.editorUI.clearSelection();
2072
+ }
2073
+ if (self.editorUI.selectedWrapperId && !self.template.getWrapperById(self.editorUI.selectedWrapperId)) {
2074
+ self.editorUI.clearSelection();
2075
+ }
2076
+ if (self.editorUI.selectedSubColumnId && !this.findSubColumn(self.editorUI.selectedSubColumnId)) {
2077
+ self.editorUI.clearSelection();
2078
+ }
2079
+ if (self.editorUI.pendingWrapperDeleteId && !self.template.getWrapperById(self.editorUI.pendingWrapperDeleteId)) {
2080
+ self.editorUI.cancelWrapperDelete();
2081
+ }
2082
+ },
2083
+ findSubColumn(id) {
2084
+ for (const section of self.template.allSections) {
2085
+ for (const column of section.columns) {
2086
+ const sc = (column.subColumns ?? []).find((s) => s.id === id);
2087
+ if (sc) return sc;
2088
+ }
2089
+ }
2090
+ return void 0;
2091
+ },
2092
+ /**
2093
+ * Load a new template
2094
+ */
2095
+ loadTemplate(templateData) {
2096
+ applySnapshot(self.template, templateData);
2097
+ self.editorUI.clearSelection();
2098
+ this.clearHistory();
2099
+ },
2100
+ /**
2101
+ * Reset to a new empty template
2102
+ */
2103
+ resetTemplate(title) {
2104
+ const newTemplate = createTemplateWithDefaultSection({ title });
2105
+ applySnapshot(self.template, newTemplate);
2106
+ self.editorUI.clearSelection();
2107
+ this.clearHistory();
2108
+ }
2109
+ })).views((self) => ({
2110
+ /**
2111
+ * Check if undo is available
2112
+ */
2113
+ get canUndo() {
2114
+ return self.historyIndex > 0;
2115
+ },
2116
+ /**
2117
+ * Check if redo is available
2118
+ */
2119
+ get canRedo() {
2120
+ return self.historyIndex < self.history.length - 1;
2121
+ },
2122
+ /**
2123
+ * Get the currently selected block. Explicit return types here
2124
+ * (and on the other selectors below) keep the inferred RootStore
2125
+ * type small enough for TypeScript's serialization limit.
2126
+ */
2127
+ get selectedBlock() {
2128
+ if (!self.editorUI.selectedBlockId) return void 0;
2129
+ return self.template.findBlockById(self.editorUI.selectedBlockId);
2130
+ },
2131
+ /**
2132
+ * Get the currently selected section
2133
+ */
2134
+ get selectedSection() {
2135
+ if (!self.editorUI.selectedSectionId) return void 0;
2136
+ return self.template.getSectionById(self.editorUI.selectedSectionId);
2137
+ },
2138
+ /**
2139
+ * Get the currently selected column
2140
+ */
2141
+ get selectedColumn() {
2142
+ if (!self.editorUI.selectedColumnId) return void 0;
2143
+ return self.template.findColumnById(self.editorUI.selectedColumnId);
2144
+ },
2145
+ /**
2146
+ * Get the currently selected sub-column (depth-2 nested).
2147
+ */
2148
+ get selectedSubColumn() {
2149
+ const id = self.editorUI.selectedSubColumnId;
2150
+ if (!id) return void 0;
2151
+ for (const section of self.template.allSections) {
2152
+ for (const column of section.columns) {
2153
+ const sc = (column.subColumns ?? []).find((s) => s.id === id);
2154
+ if (sc) return sc;
2155
+ }
2156
+ }
2157
+ return void 0;
2158
+ },
2159
+ /**
2160
+ * Get the currently selected wrapper
2161
+ */
2162
+ get selectedWrapper() {
2163
+ if (!self.editorUI.selectedWrapperId) return void 0;
2164
+ return self.template.getWrapperById(self.editorUI.selectedWrapperId);
2165
+ },
2166
+ /**
2167
+ * Get history size info
2168
+ */
2169
+ get historyInfo() {
2170
+ return {
2171
+ current: self.historyIndex + 1,
2172
+ total: self.history.length,
2173
+ canUndo: self.historyIndex > 0,
2174
+ canRedo: self.historyIndex < self.history.length - 1
2175
+ };
2176
+ }
2177
+ }));
2178
+ function createRootStore(options = {}) {
2179
+ const { template: initialTemplate, onChange, onChangeDebounce = 300 } = options;
2180
+ const store = RootStore.create({
2181
+ template: initialTemplate || createTemplateWithDefaultSection(),
2182
+ editorUI: {}
2183
+ });
2184
+ store.initHistory();
2185
+ if (onChange) {
2186
+ let timeoutId;
2187
+ onSnapshot(store.template, (snapshot) => {
2188
+ store.recordHistory();
2189
+ if (timeoutId) clearTimeout(timeoutId);
2190
+ timeoutId = setTimeout(() => {
2191
+ onChange(snapshot);
2192
+ }, onChangeDebounce);
2193
+ });
2194
+ } else {
2195
+ onSnapshot(store.template, () => {
2196
+ store.recordHistory();
2197
+ });
2198
+ }
2199
+ return store;
2200
+ }
2201
+ function createEmptyStore() {
2202
+ return createRootStore({
2203
+ template: createTemplate()
2204
+ });
2205
+ }
2206
+ export {
2207
+ AccordionBlockSchema,
2208
+ AccordionItemModel,
2209
+ BackgroundGradientSchema,
2210
+ BlockModel,
2211
+ BlockRegistryImpl,
2212
+ BlockSchema,
2213
+ BlockType,
2214
+ ButtonBlockSchema,
2215
+ CONTAINER_BLOCK_TYPES,
2216
+ CURRENT_TEMPLATE_VERSION,
2217
+ CarouselBlockSchema,
2218
+ CarouselImageModel,
2219
+ ColumnModel,
2220
+ ColumnSchema,
2221
+ CustomFontSchema,
2222
+ DividerBlockSchema,
2223
+ EditorUIStore,
2224
+ EmailTemplateSchema,
2225
+ EmailTemplateSchemaV1_0,
2226
+ EmailTemplateSchemaV1_1,
2227
+ ExtraAttributesSchema,
2228
+ FontDefinitionModel,
2229
+ FooterBlockSchema,
2230
+ GradientStopSchema,
2231
+ HeaderBlockSchema,
2232
+ HeroBlockSchema,
2233
+ HistoryManager,
2234
+ ImageBlockSchema,
2235
+ LEAF_BLOCK_TYPES,
2236
+ MJML_WRAPPER_DEFAULT_PADDING,
2237
+ MjmlHeadSchema,
2238
+ NavbarBlockSchema,
2239
+ NavbarLinkModel,
2240
+ RawBlockSchema,
2241
+ RootStore,
2242
+ SUPPORTED_TEMPLATE_VERSIONS,
2243
+ SectionModel,
2244
+ SectionSchema,
2245
+ SectionSchemaV1_0,
2246
+ SelectionManager,
2247
+ SocialBlockSchema,
2248
+ SocialLinkModel,
2249
+ SpacerBlockSchema,
2250
+ SpacingModel,
2251
+ SpacingSchema,
2252
+ SubColumnModel,
2253
+ TableBlockSchema,
2254
+ TemplateMetadataModel,
2255
+ TemplateMetadataSchema,
2256
+ TemplateMigrationError,
2257
+ TemplateModel,
2258
+ TextBlockSchema,
2259
+ ThemeColorModel,
2260
+ TopLevelItemModel,
2261
+ TopLevelItemSchema,
2262
+ WrapperModel,
2263
+ WrapperSchema,
2264
+ allSections,
2265
+ buildGradientCSS,
2266
+ cloneSectionSnapshot,
2267
+ createBlockRegistry,
2268
+ createColumn,
2269
+ createEmptyStore,
2270
+ createHistoryManager,
2271
+ createPrebuiltTemplateRegistry,
2272
+ createRootStore,
2273
+ createSection,
2274
+ createSelectionManager,
2275
+ createSubColumn,
2276
+ createTemplate,
2277
+ createTemplateWithDefaultSection,
2278
+ createWrapper,
2279
+ isLeafBlockType,
2280
+ isTemplateMigrationError,
2281
+ isWrapper,
2282
+ isWrapperInstance,
2283
+ migrateTemplate,
2284
+ migrateV1_0ToV1_1,
2285
+ validateTemplate,
2286
+ withTemplateId
2287
+ };