@mjml-agent-editor/editor 0.1.17 → 0.3.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/canvas.cjs CHANGED
@@ -31,16 +31,17 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
31
31
  // src/canvas.ts
32
32
  var canvas_exports = {};
33
33
  __export(canvas_exports, {
34
- EmailEditor: () => EmailEditor
34
+ EmailEditor: () => EmailEditor,
35
+ blockProblems: () => blockProblems
35
36
  });
36
37
  module.exports = __toCommonJS(canvas_exports);
37
38
 
38
39
  // src/components/editor/EmailEditor.tsx
39
- var import_grapesjs = __toESM(require("grapesjs"), 1);
40
- var import_react6 = __toESM(require("@grapesjs/react"), 1);
41
- var import_grapesjs_mjml = __toESM(require("grapesjs-mjml"), 1);
42
- var import_react7 = require("react");
43
- var import_core2 = require("@mjml-agent-editor/core");
40
+ var import_grapesjs2 = __toESM(require("grapesjs"), 1);
41
+ var import_react9 = __toESM(require("@grapesjs/react"), 1);
42
+ var import_grapesjs_mjml2 = __toESM(require("grapesjs-mjml"), 1);
43
+ var import_react10 = require("react");
44
+ var import_core4 = require("@mjml-agent-editor/core");
44
45
 
45
46
  // src/stores.tsx
46
47
  var import_react = require("react");
@@ -63,9 +64,204 @@ var DEFAULT_LABELS = {
63
64
  exportHtml: "Export HTML",
64
65
  exportMjml: "Export MJML",
65
66
  copied: (kind) => `${kind} copied.`,
67
+ tabBlocks: "Blocks",
68
+ tabSettings: "Settings",
69
+ tabLayers: "Layers",
66
70
  searchBlocks: "Search blocks\u2026",
71
+ noBlocksMatch: "No blocks match.",
72
+ blockAdded: (block) => `Added: ${block}`,
73
+ blockCategoryLayout: "Layout",
74
+ blockCategoryContent: "Content",
75
+ blockCategorySections: "Sections",
76
+ blockLabels: {
77
+ "mj-1-column": "1 column",
78
+ "mj-2-columns": "2 columns",
79
+ "mj-3-columns": "3 columns",
80
+ "mj-text": "Text",
81
+ "mj-image": "Image",
82
+ "mj-button": "Button",
83
+ "mj-divider": "Divider",
84
+ "mj-spacer": "Spacer",
85
+ "mj-social-group": "Social links",
86
+ "mj-navbar": "Navigation"
87
+ },
88
+ elementNames: {
89
+ "mj-wrapper": "Section group",
90
+ "mj-section": "Section",
91
+ "mj-column": "Column",
92
+ "mj-group": "Column group",
93
+ "mj-text": "Text",
94
+ "mj-button": "Button",
95
+ "mj-image": "Image",
96
+ "mj-divider": "Divider",
97
+ "mj-spacer": "Spacer",
98
+ "mj-hero": "Hero",
99
+ "mj-navbar": "Navigation",
100
+ "mj-navbar-link": "Navigation link",
101
+ "mj-social": "Social links",
102
+ "mj-social-element": "Social link",
103
+ "mj-raw": "Raw HTML",
104
+ "mj-table": "Table"
105
+ },
67
106
  expandAll: "Expand all",
68
107
  selectAnElement: "Select an element\u2026",
108
+ settingsEmpty: "Select something in the email to edit it.",
109
+ emailSettings: "Email settings",
110
+ loadingEditor: "Loading editor\u2026",
111
+ commentOnElement: "Comment on element / section",
112
+ groupContent: "Content",
113
+ groupLayout: "Layout",
114
+ groupSpacing: "Spacing",
115
+ groupStyle: "Style",
116
+ groupAdvanced: "Advanced",
117
+ otherAttributes: "Other attributes",
118
+ removeAttribute: "Remove",
119
+ resetToDefault: "Reset to default",
120
+ selectParent: "Select parent",
121
+ duplicate: "Duplicate",
122
+ deleteElement: "Delete",
123
+ multipleSelected: (count) => `${count} elements selected \u2014 select one to edit its settings.`,
124
+ editEachSide: "Edit each side",
125
+ increase: "Increase",
126
+ decrease: "Decrease",
127
+ changeUnit: (unit) => `Unit: ${unit}. Click to change`,
128
+ noUnit: "\xD7",
129
+ sidesVertical: "Top and bottom",
130
+ sidesHorizontal: "Left and right",
131
+ sideTop: "Top",
132
+ sideRight: "Right",
133
+ sideBottom: "Bottom",
134
+ sideLeft: "Left",
135
+ linkTypeProfile: "Your profile",
136
+ linkTypeShare: "Share this page",
137
+ allScreenSizes: "Changes apply to every screen size.",
138
+ columnWidths: "Column widths",
139
+ columnRatio: (ratio) => `Columns ${ratio}`,
140
+ socialNetworks: "Networks",
141
+ addNetwork: "Add network",
142
+ navbarLinks: "Links",
143
+ addLink: "Add link",
144
+ newLinkText: "Link",
145
+ inspectorLabels: {
146
+ // Fields
147
+ contentWidth: "Width",
148
+ pageBackground: "Page background",
149
+ "pageBackground.help": "Fills the area around and behind the email.",
150
+ fullWidth: "Full-width background",
151
+ "fullWidth.help": "The background reaches the window edges; the content keeps the email width.",
152
+ padding: "Padding",
153
+ backgroundColor: "Background",
154
+ backgroundImage: "Background image",
155
+ backgroundSize: "Image size",
156
+ backgroundRepeat: "Repeat image",
157
+ backgroundPosition: "Position",
158
+ cornerRadius: "Corners",
159
+ gap: "Space between sections",
160
+ border: "Border",
161
+ alignColumns: "Align columns",
162
+ "alignColumns.help": "Where the columns sit when they don't fill the row.",
163
+ reverseColumns: "Reverse column order",
164
+ "reverseColumns.help": "Swaps the columns on desktop; on phones they still stack in their original order.",
165
+ verticalAlign: "Vertical",
166
+ width: "Width",
167
+ innerBackground: "Inner background",
168
+ "innerBackground.help": "Fills only the area inside the padding, so it needs padding to show.",
169
+ innerCornerRadius: "Inner corners",
170
+ innerBorder: "Inner border",
171
+ align: "Alignment",
172
+ font: "Font",
173
+ fontSize: "Text size",
174
+ fontWeight: "Weight",
175
+ textColor: "Text color",
176
+ lineHeight: "Line height",
177
+ letterSpacing: "Letter spacing",
178
+ textTransform: "Capitalization",
179
+ blockBackground: "Block background",
180
+ "blockBackground.help": "Fills the whole block, including its padding.",
181
+ link: "Link",
182
+ buttonColor: "Button color",
183
+ buttonPadding: "Button padding",
184
+ "buttonPadding.help": "Space between the text and the button's edge; this sets the button's size.",
185
+ spaceAround: "Space around",
186
+ "spaceAround.help": "Empty space outside the button, between it and what is next to it.",
187
+ opensIn: "Opens in",
188
+ tooltip: "Tooltip",
189
+ rel: "Link relationship",
190
+ image: "Image",
191
+ altText: "Alt text",
192
+ "altText.help": "Read by screen readers and shown when images are blocked.",
193
+ fluidOnMobile: "Full width on mobile",
194
+ "fluidOnMobile.help": "On phones the image stretches across the screen, whatever its width here.",
195
+ lineColor: "Line color",
196
+ thickness: "Thickness",
197
+ lineStyle: "Line style",
198
+ height: "Height",
199
+ socialMode: "Direction",
200
+ iconSize: "Icon size",
201
+ iconSpacing: "Space around each icon",
202
+ network: "Network",
203
+ // Brand names are the same in every language and are not keys; these two are words.
204
+ networkWebsite: "Website",
205
+ networkCustom: "Custom",
206
+ customIcon: "Custom icon",
207
+ iconColor: "Icon color",
208
+ iconPosition: "Icon side",
209
+ heroMode: "Sizing",
210
+ "heroMode.help": "Fixed keeps the height you set; Fit image follows the background image's proportions.",
211
+ backgroundWidth: "Image width",
212
+ "backgroundWidth.help": "The background image's real size in pixels, so the hero keeps its proportions.",
213
+ backgroundHeight: "Image height",
214
+ fallbackColor: "Fallback color",
215
+ "fallbackColor.help": "Shown while the image loads, or when images are blocked.",
216
+ contentPadding: "Content padding",
217
+ contentBackground: "Content background",
218
+ hamburger: "Collapse into a menu on mobile",
219
+ "hamburger.help": "On phones the links fold into a menu button.",
220
+ menuIconColor: "Menu icon color",
221
+ menuIconSize: "Menu icon size",
222
+ menuIconAlign: "Menu icon align",
223
+ baseUrl: "Link prefix",
224
+ "baseUrl.help": "Added in front of every link in the bar, full addresses included, so links then give only the path.",
225
+ // Options
226
+ alignLeft: "Left",
227
+ alignCenter: "Center",
228
+ alignRight: "Right",
229
+ alignJustify: "Justify",
230
+ alignTop: "Top",
231
+ alignMiddle: "Middle",
232
+ alignBottom: "Bottom",
233
+ weightRegular: "Regular",
234
+ weightMedium: "Medium",
235
+ weightSemibold: "Semibold",
236
+ weightBold: "Bold",
237
+ caseNone: "As typed",
238
+ caseUpper: "UPPERCASE",
239
+ caseLower: "lowercase",
240
+ caseCapitalize: "Capitalize Each Word",
241
+ targetNewTab: "New tab",
242
+ targetSameTab: "Same tab",
243
+ sizeCover: "Fill",
244
+ sizeContain: "Fit",
245
+ sizeAuto: "Original",
246
+ repeatTile: "Tile",
247
+ repeatOnce: "Once",
248
+ positionTopLeft: "Top left",
249
+ positionTopCenter: "Top",
250
+ positionTopRight: "Top right",
251
+ positionCenterLeft: "Left",
252
+ positionCenter: "Center",
253
+ positionCenterRight: "Right",
254
+ positionBottomLeft: "Bottom left",
255
+ positionBottomCenter: "Bottom",
256
+ positionBottomRight: "Bottom right",
257
+ lineSolid: "Solid",
258
+ lineDashed: "Dashed",
259
+ lineDotted: "Dotted",
260
+ modeHorizontal: "In a row",
261
+ modeVertical: "Stacked",
262
+ heroFixed: "Fixed",
263
+ heroFluid: "Fit image"
264
+ },
69
265
  attributes: "Attributes",
70
266
  style: "Style",
71
267
  rteFont: "Font",
@@ -132,7 +328,10 @@ function mergeLabels(overrides) {
132
328
  return {
133
329
  ...DEFAULT_LABELS,
134
330
  ...overrides,
135
- toolLabels: { ...DEFAULT_LABELS.toolLabels, ...overrides.toolLabels }
331
+ toolLabels: { ...DEFAULT_LABELS.toolLabels, ...overrides.toolLabels },
332
+ blockLabels: { ...DEFAULT_LABELS.blockLabels, ...overrides.blockLabels },
333
+ elementNames: { ...DEFAULT_LABELS.elementNames, ...overrides.elementNames },
334
+ inspectorLabels: { ...DEFAULT_LABELS.inspectorLabels, ...overrides.inspectorLabels }
136
335
  };
137
336
  }
138
337
 
@@ -255,11 +454,58 @@ function Input({ className, type, ...props }) {
255
454
  );
256
455
  }
257
456
 
457
+ // src/components/ui/tooltip.tsx
458
+ var React3 = require("react");
459
+ var import_radix_ui2 = require("radix-ui");
460
+ var import_jsx_runtime4 = require("react/jsx-runtime");
461
+ function TooltipProvider({
462
+ delayDuration = 0,
463
+ ...props
464
+ }) {
465
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
466
+ import_radix_ui2.Tooltip.Provider,
467
+ {
468
+ "data-slot": "tooltip-provider",
469
+ delayDuration,
470
+ ...props
471
+ }
472
+ );
473
+ }
474
+ function Tooltip({ ...props }) {
475
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_radix_ui2.Tooltip.Root, { "data-slot": "tooltip", ...props });
476
+ }
477
+ function TooltipTrigger({ ...props }) {
478
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_radix_ui2.Tooltip.Trigger, { "data-slot": "tooltip-trigger", ...props });
479
+ }
480
+ function TooltipContent({
481
+ className,
482
+ sideOffset = 0,
483
+ children,
484
+ ...props
485
+ }) {
486
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_radix_ui2.Tooltip.Portal, { children: /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
487
+ import_radix_ui2.Tooltip.Content,
488
+ {
489
+ "data-slot": "tooltip-content",
490
+ sideOffset,
491
+ className: cn(
492
+ "z-50 inline-flex w-fit max-w-xs origin-(--radix-tooltip-content-transform-origin) items-center gap-1.5 rounded-md bg-foreground px-3 py-1.5 text-xs text-background has-data-[slot=kbd]:pr-1.5 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 **:data-[slot=kbd]:relative **:data-[slot=kbd]:isolate **:data-[slot=kbd]:z-50 **:data-[slot=kbd]:rounded-sm data-[state=delayed-open]:animate-in data-[state=delayed-open]:fade-in-0 data-[state=delayed-open]:zoom-in-95 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
493
+ className
494
+ ),
495
+ ...props,
496
+ children: [
497
+ children,
498
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_radix_ui2.Tooltip.Arrow, { className: "z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px] bg-foreground fill-foreground" })
499
+ ]
500
+ }
501
+ ) });
502
+ }
503
+
258
504
  // src/components/ui/spinner.tsx
259
505
  var import_lucide_react = require("lucide-react");
260
- var import_jsx_runtime4 = require("react/jsx-runtime");
506
+ var import_jsx_runtime5 = require("react/jsx-runtime");
261
507
  function Spinner({ className, ...props }) {
262
- return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
508
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
263
509
  import_lucide_react.Loader2Icon,
264
510
  {
265
511
  "data-slot": "spinner",
@@ -272,17 +518,17 @@ function Spinner({ className, ...props }) {
272
518
  }
273
519
 
274
520
  // src/components/ui/popover.tsx
275
- var React3 = require("react");
276
- var import_radix_ui2 = require("radix-ui");
277
- var import_jsx_runtime5 = require("react/jsx-runtime");
521
+ var React4 = require("react");
522
+ var import_radix_ui3 = require("radix-ui");
523
+ var import_jsx_runtime6 = require("react/jsx-runtime");
278
524
  function Popover({ ...props }) {
279
- return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_radix_ui2.Popover.Root, { "data-slot": "popover", ...props });
525
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_radix_ui3.Popover.Root, { "data-slot": "popover", ...props });
280
526
  }
281
527
  function PopoverTrigger({ ...props }) {
282
- return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_radix_ui2.Popover.Trigger, { "data-slot": "popover-trigger", ...props });
528
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_radix_ui3.Popover.Trigger, { "data-slot": "popover-trigger", ...props });
283
529
  }
284
530
  function PopoverAnchor({ ...props }) {
285
- return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_radix_ui2.Popover.Anchor, { "data-slot": "popover-anchor", ...props });
531
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_radix_ui3.Popover.Anchor, { "data-slot": "popover-anchor", ...props });
286
532
  }
287
533
  function PopoverContent({
288
534
  className,
@@ -290,8 +536,8 @@ function PopoverContent({
290
536
  sideOffset = 4,
291
537
  ...props
292
538
  }) {
293
- return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_radix_ui2.Popover.Portal, { children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
294
- import_radix_ui2.Popover.Content,
539
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_radix_ui3.Popover.Portal, { children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
540
+ import_radix_ui3.Popover.Content,
295
541
  {
296
542
  "data-slot": "popover-content",
297
543
  align,
@@ -306,22 +552,22 @@ function PopoverContent({
306
552
  }
307
553
 
308
554
  // src/components/ui/dialog.tsx
309
- var React4 = require("react");
310
- var import_radix_ui3 = require("radix-ui");
555
+ var React5 = require("react");
556
+ var import_radix_ui4 = require("radix-ui");
311
557
  var import_lucide_react2 = require("lucide-react");
312
- var import_jsx_runtime6 = require("react/jsx-runtime");
558
+ var import_jsx_runtime7 = require("react/jsx-runtime");
313
559
  function Dialog({ ...props }) {
314
- return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_radix_ui3.Dialog.Root, { "data-slot": "dialog", ...props });
560
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_radix_ui4.Dialog.Root, { "data-slot": "dialog", ...props });
315
561
  }
316
562
  function DialogPortal({ ...props }) {
317
- return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_radix_ui3.Dialog.Portal, { "data-slot": "dialog-portal", ...props });
563
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_radix_ui4.Dialog.Portal, { "data-slot": "dialog-portal", ...props });
318
564
  }
319
565
  function DialogOverlay({
320
566
  className,
321
567
  ...props
322
568
  }) {
323
- return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
324
- import_radix_ui3.Dialog.Overlay,
569
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
570
+ import_radix_ui4.Dialog.Overlay,
325
571
  {
326
572
  "data-slot": "dialog-overlay",
327
573
  className: cn(
@@ -338,10 +584,10 @@ function DialogContent({
338
584
  showCloseButton = true,
339
585
  ...props
340
586
  }) {
341
- return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(DialogPortal, { children: [
342
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(DialogOverlay, {}),
343
- /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
344
- import_radix_ui3.Dialog.Content,
587
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(DialogPortal, { children: [
588
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(DialogOverlay, {}),
589
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(
590
+ import_radix_ui4.Dialog.Content,
345
591
  {
346
592
  "data-slot": "dialog-content",
347
593
  className: cn(
@@ -351,9 +597,9 @@ function DialogContent({
351
597
  ...props,
352
598
  children: [
353
599
  children,
354
- showCloseButton && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_radix_ui3.Dialog.Close, { "data-slot": "dialog-close", asChild: true, children: /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(Button, { variant: "ghost", className: "absolute top-2 right-2", size: "icon-sm", children: [
355
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_lucide_react2.XIcon, {}),
356
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "sr-only", children: "Close" })
600
+ showCloseButton && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_radix_ui4.Dialog.Close, { "data-slot": "dialog-close", asChild: true, children: /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(Button, { variant: "ghost", className: "absolute top-2 right-2", size: "icon-sm", children: [
601
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_lucide_react2.XIcon, {}),
602
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { className: "sr-only", children: "Close" })
357
603
  ] }) })
358
604
  ]
359
605
  }
@@ -361,11 +607,11 @@ function DialogContent({
361
607
  ] });
362
608
  }
363
609
  function DialogHeader({ className, ...props }) {
364
- return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { "data-slot": "dialog-header", className: cn("flex flex-col gap-2", className), ...props });
610
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { "data-slot": "dialog-header", className: cn("flex flex-col gap-2", className), ...props });
365
611
  }
366
612
  function DialogTitle({ className, ...props }) {
367
- return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
368
- import_radix_ui3.Dialog.Title,
613
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
614
+ import_radix_ui4.Dialog.Title,
369
615
  {
370
616
  "data-slot": "dialog-title",
371
617
  className: cn("font-heading text-base leading-none font-medium", className),
@@ -377,8 +623,8 @@ function DialogDescription({
377
623
  className,
378
624
  ...props
379
625
  }) {
380
- return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
381
- import_radix_ui3.Dialog.Description,
626
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
627
+ import_radix_ui4.Dialog.Description,
382
628
  {
383
629
  "data-slot": "dialog-description",
384
630
  className: cn(
@@ -412,10 +658,10 @@ function saveToastId(docId) {
412
658
  }
413
659
 
414
660
  // src/components/ui/textarea.tsx
415
- var React5 = require("react");
416
- var import_jsx_runtime7 = require("react/jsx-runtime");
661
+ var React6 = require("react");
662
+ var import_jsx_runtime8 = require("react/jsx-runtime");
417
663
  function Textarea({ className, ...props }) {
418
- return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
664
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
419
665
  "textarea",
420
666
  {
421
667
  "data-slot": "textarea",
@@ -432,7 +678,7 @@ function Textarea({ className, ...props }) {
432
678
  var import_react2 = require("@grapesjs/react");
433
679
  var import_react3 = require("react");
434
680
  var import_lucide_react3 = require("lucide-react");
435
- var import_jsx_runtime8 = require("react/jsx-runtime");
681
+ var import_jsx_runtime9 = require("react/jsx-runtime");
436
682
  var keyOf = (c) => c.objectId ? `obj-${c.objectId}` : `sec-${c.sectionId}`;
437
683
  var keyOfTarget = (t) => t.objectId ? `obj-${t.objectId}` : `sec-${t.sectionId}`;
438
684
  function CanvasComments({
@@ -586,11 +832,11 @@ function CanvasComments({
586
832
  console.error(e);
587
833
  }
588
834
  };
589
- return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { ref: containerRef, className: "pointer-events-none absolute inset-0 z-20 overflow-hidden", children: [
835
+ return /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { ref: containerRef, className: "pointer-events-none absolute inset-0 z-20 overflow-hidden", children: [
590
836
  pins.map(({ key, list }) => {
591
837
  const pos = positions[key];
592
838
  if (!pos) return null;
593
- return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
839
+ return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
594
840
  "button",
595
841
  {
596
842
  type: "button",
@@ -606,8 +852,8 @@ function CanvasComments({
606
852
  key
607
853
  );
608
854
  }),
609
- /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(Popover, { open: !!activeKey && !!activePos, onOpenChange: (o) => !o && closePopover(), children: [
610
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(PopoverAnchor, { asChild: true, children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
855
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(Popover, { open: !!activeKey && !!activePos, onOpenChange: (o) => !o && closePopover(), children: [
856
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(PopoverAnchor, { asChild: true, children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
611
857
  "div",
612
858
  {
613
859
  style: {
@@ -619,45 +865,45 @@ function CanvasComments({
619
865
  }
620
866
  }
621
867
  ) }),
622
- /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
868
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(
623
869
  PopoverContent,
624
870
  {
625
871
  side: "right",
626
872
  align: "start",
627
873
  className: "pointer-events-auto w-80 p-0 data-[state=closed]:!animate-none data-[state=closed]:!duration-0",
628
874
  children: [
629
- /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "flex items-start justify-between gap-2 border-b px-3 py-2 text-xs text-muted-foreground", children: [
630
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { className: "min-w-0", children: activeTarget?.objectId ? /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(import_jsx_runtime8.Fragment, { children: [
875
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { className: "flex items-start justify-between gap-2 border-b px-3 py-2 text-xs text-muted-foreground", children: [
876
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { className: "min-w-0", children: activeTarget?.objectId ? /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(import_jsx_runtime9.Fragment, { children: [
631
877
  "Comment on",
632
878
  " ",
633
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { className: "font-medium text-foreground", children: activeTarget.objectLabel })
634
- ] }) : /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(import_jsx_runtime8.Fragment, { children: [
879
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { className: "font-medium text-foreground", children: activeTarget.objectLabel })
880
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(import_jsx_runtime9.Fragment, { children: [
635
881
  labels.commentOnSection,
636
882
  " ",
637
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("code", { className: "font-mono", children: activeTarget?.sectionId })
883
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("code", { className: "font-mono", children: activeTarget?.sectionId })
638
884
  ] }) }),
639
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
885
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
640
886
  "button",
641
887
  {
642
888
  type: "button",
643
889
  onClick: closePopover,
644
890
  "aria-label": labels.closeComments,
645
891
  className: "-mr-1 -mt-0.5 shrink-0 rounded p-0.5 text-muted-foreground hover:bg-muted hover:text-foreground",
646
- children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_lucide_react3.X, { className: "size-4" })
892
+ children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(import_lucide_react3.X, { className: "size-4" })
647
893
  }
648
894
  )
649
895
  ] }),
650
- /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "max-h-64 space-y-2 overflow-y-auto p-3", children: [
651
- activeComments.length === 0 && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("p", { className: "text-xs text-muted-foreground", children: labels.noCommentsYet }),
652
- activeComments.map((c) => /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "rounded-md border bg-muted/40 p-2 text-sm", children: [
653
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("p", { className: "whitespace-pre-wrap", children: c.body }),
654
- /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
896
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { className: "max-h-64 space-y-2 overflow-y-auto p-3", children: [
897
+ activeComments.length === 0 && /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("p", { className: "text-xs text-muted-foreground", children: labels.noCommentsYet }),
898
+ activeComments.map((c) => /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { className: "rounded-md border bg-muted/40 p-2 text-sm", children: [
899
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("p", { className: "whitespace-pre-wrap", children: c.body }),
900
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(
655
901
  "button",
656
902
  {
657
903
  onClick: () => void resolve(c.id),
658
904
  className: "mt-1 inline-flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground",
659
905
  children: [
660
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_lucide_react3.Check, { className: "size-3" }),
906
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(import_lucide_react3.Check, { className: "size-3" }),
661
907
  " ",
662
908
  labels.resolveComment
663
909
  ]
@@ -665,8 +911,8 @@ function CanvasComments({
665
911
  )
666
912
  ] }, c.id))
667
913
  ] }),
668
- /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "border-t p-3", children: [
669
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
914
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { className: "border-t p-3", children: [
915
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
670
916
  Textarea,
671
917
  {
672
918
  autoFocus: true,
@@ -682,8 +928,8 @@ function CanvasComments({
682
928
  }
683
929
  }
684
930
  ),
685
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { className: "mt-2 flex justify-end", children: /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(Button, { size: "sm", disabled: !body.trim(), onClick: () => void submit(), children: [
686
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_lucide_react3.MessageSquarePlus, {}),
931
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { className: "mt-2 flex justify-end", children: /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(Button, { size: "sm", disabled: !body.trim(), onClick: () => void submit(), children: [
932
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(import_lucide_react3.MessageSquarePlus, {}),
687
933
  " ",
688
934
  labels.addComment
689
935
  ] }) })
@@ -695,6 +941,30 @@ function CanvasComments({
695
941
  ] });
696
942
  }
697
943
 
944
+ // src/lib/save-baseline.ts
945
+ function createSaveBaseline() {
946
+ let stored = "";
947
+ let loaded = null;
948
+ return {
949
+ /** The MJML the store holds, as far as this editor knows. */
950
+ get stored() {
951
+ return stored;
952
+ },
953
+ /** `canvas` is what the load serialises to, or null when it repaired something to write. */
954
+ load(mjml, canvas) {
955
+ stored = mjml;
956
+ loaded = canvas;
957
+ },
958
+ saved(mjml) {
959
+ stored = mjml;
960
+ loaded = null;
961
+ },
962
+ changed(mjml) {
963
+ return mjml !== stored && mjml !== loaded;
964
+ }
965
+ };
966
+ }
967
+
698
968
  // src/lib/save-queue.ts
699
969
  function createSaveQueue(save, delayMs) {
700
970
  let timer = null;
@@ -724,21 +994,62 @@ function createSaveQueue(save, delayMs) {
724
994
  };
725
995
  }
726
996
 
997
+ // src/lib/turn-follower.ts
998
+ function createTurnFollower() {
999
+ let running = false;
1000
+ let mounted = true;
1001
+ let apply = null;
1002
+ return {
1003
+ turn(on) {
1004
+ if (on === running) return;
1005
+ running = on;
1006
+ if (mounted) apply?.(on);
1007
+ },
1008
+ /**
1009
+ * Whether the canvas is still mounted. An unmounted one's late reload must not unlock
1010
+ * the shared state a canvas mounted since has locked.
1011
+ */
1012
+ get mounted() {
1013
+ return mounted;
1014
+ },
1015
+ // Both, as React mounts twice in development.
1016
+ mount() {
1017
+ mounted = true;
1018
+ },
1019
+ unmount() {
1020
+ mounted = false;
1021
+ },
1022
+ /** A turn already under way is applied at once; none means nothing to apply. */
1023
+ ready(fn) {
1024
+ apply = fn;
1025
+ if (running) fn(true);
1026
+ }
1027
+ };
1028
+ }
1029
+
1030
+ // src/lib/shortcuts.ts
1031
+ function opensBlockSearch(e) {
1032
+ if (e.key !== "/" || e.metaKey || e.ctrlKey || e.altKey || e.isComposing) return false;
1033
+ const target = e.target;
1034
+ if (target?.isContentEditable) return false;
1035
+ return !["INPUT", "TEXTAREA", "SELECT", "IFRAME"].includes(target?.tagName ?? "");
1036
+ }
1037
+
727
1038
  // src/components/editor/EmailEditor.tsx
728
1039
  var import_sonner2 = require("sonner");
729
- var import_lucide_react7 = require("lucide-react");
1040
+ var import_lucide_react10 = require("lucide-react");
730
1041
 
731
1042
  // src/components/ui/switch.tsx
732
- var React6 = require("react");
733
- var import_radix_ui4 = require("radix-ui");
734
- var import_jsx_runtime9 = require("react/jsx-runtime");
1043
+ var React7 = require("react");
1044
+ var import_radix_ui5 = require("radix-ui");
1045
+ var import_jsx_runtime10 = require("react/jsx-runtime");
735
1046
  function Switch({
736
1047
  className,
737
1048
  size = "default",
738
1049
  ...props
739
1050
  }) {
740
- return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
741
- import_radix_ui4.Switch.Root,
1051
+ return /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
1052
+ import_radix_ui5.Switch.Root,
742
1053
  {
743
1054
  "data-slot": "switch",
744
1055
  "data-size": size,
@@ -747,8 +1058,8 @@ function Switch({
747
1058
  className
748
1059
  ),
749
1060
  ...props,
750
- children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
751
- import_radix_ui4.Switch.Thumb,
1061
+ children: /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
1062
+ import_radix_ui5.Switch.Thumb,
752
1063
  {
753
1064
  "data-slot": "switch-thumb",
754
1065
  className: "pointer-events-none block rounded-full bg-background ring-0 transition-transform group-data-[size=default]/switch:size-4 group-data-[size=sm]/switch:size-3 group-data-[size=default]/switch:data-checked:translate-x-[calc(100%-2px)] group-data-[size=sm]/switch:data-checked:translate-x-[calc(100%-2px)] dark:data-checked:bg-primary-foreground group-data-[size=default]/switch:data-unchecked:translate-x-0 group-data-[size=sm]/switch:data-unchecked:translate-x-0 dark:data-unchecked:bg-foreground"
@@ -759,13 +1070,13 @@ function Switch({
759
1070
  }
760
1071
 
761
1072
  // src/components/editor/EmailEditor.tsx
762
- var import_lucide_react8 = require("lucide-react");
1073
+ var import_lucide_react11 = require("lucide-react");
763
1074
 
764
1075
  // src/components/editor/ImagePicker.tsx
765
1076
  var import_react4 = require("react");
766
1077
  var import_lucide_react4 = require("lucide-react");
767
1078
  var import_sonner = require("sonner");
768
- var import_jsx_runtime10 = require("react/jsx-runtime");
1079
+ var import_jsx_runtime11 = require("react/jsx-runtime");
769
1080
  function ImagePicker({
770
1081
  open,
771
1082
  currentUrl,
@@ -857,13 +1168,13 @@ function ImagePicker({
857
1168
  setDragging(false);
858
1169
  if (!full) void upload(e.dataTransfer.files);
859
1170
  };
860
- return /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(Dialog, { open, onOpenChange: (next) => !next && onClose(), children: /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(DialogContent, { className: "editor-dark gap-0 overflow-hidden border border-panel-border bg-panel p-0 text-panel-fg ring-0 sm:max-w-3xl", children: [
861
- /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(DialogHeader, { className: "border-b border-panel-border px-5 py-4", children: [
862
- /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(DialogTitle, { children: labels.imagePickerTitle }),
863
- quota && /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(DialogDescription, { className: "text-panel-muted-fg", children: labels.imagePickerQuota(quota.used, quota.limit) })
1171
+ return /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(Dialog, { open, onOpenChange: (next) => !next && onClose(), children: /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(DialogContent, { className: "editor-dark gap-0 overflow-hidden border border-panel-border bg-panel p-0 text-panel-fg ring-0 sm:max-w-3xl", children: [
1172
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(DialogHeader, { className: "border-b border-panel-border px-5 py-4", children: [
1173
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(DialogTitle, { children: labels.imagePickerTitle }),
1174
+ quota && /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(DialogDescription, { className: "text-panel-muted-fg", children: labels.imagePickerQuota(quota.used, quota.limit) })
864
1175
  ] }),
865
- /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("div", { className: "flex max-h-[70vh] flex-col gap-4 overflow-y-auto p-5", children: [
866
- library && (full ? /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("p", { className: "rounded-lg border border-panel-border bg-panel-elevated px-4 py-3 text-sm text-panel-muted-fg", children: labels.imagePickerQuotaFull }) : /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(
1176
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { className: "flex max-h-[70vh] flex-col gap-4 overflow-y-auto p-5", children: [
1177
+ library && (full ? /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("p", { className: "rounded-lg border border-panel-border bg-panel-elevated px-4 py-3 text-sm text-panel-muted-fg", children: labels.imagePickerQuotaFull }) : /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(
867
1178
  "div",
868
1179
  {
869
1180
  onDragOver: (e) => {
@@ -877,10 +1188,10 @@ function ImagePicker({
877
1188
  dragging ? "border-brand bg-brand-soft" : "border-panel-border bg-panel-elevated"
878
1189
  ),
879
1190
  children: [
880
- /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_lucide_react4.ImageUp, { className: "size-8 text-panel-muted-fg" }),
881
- /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("div", { className: "flex flex-wrap items-center justify-center gap-2 text-sm text-panel-muted-fg", children: [
882
- /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("span", { children: labels.imagePickerDrop }),
883
- /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
1191
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_lucide_react4.ImageUp, { className: "size-8 text-panel-muted-fg" }),
1192
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { className: "flex flex-wrap items-center justify-center gap-2 text-sm text-panel-muted-fg", children: [
1193
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("span", { children: labels.imagePickerDrop }),
1194
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
884
1195
  Button,
885
1196
  {
886
1197
  type: "button",
@@ -891,7 +1202,7 @@ function ImagePicker({
891
1202
  }
892
1203
  )
893
1204
  ] }),
894
- /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
1205
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
895
1206
  "input",
896
1207
  {
897
1208
  ref: fileInput,
@@ -908,7 +1219,7 @@ function ImagePicker({
908
1219
  ]
909
1220
  }
910
1221
  )),
911
- /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(
1222
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(
912
1223
  "form",
913
1224
  {
914
1225
  className: "flex gap-2",
@@ -918,9 +1229,9 @@ function ImagePicker({
918
1229
  if (value) onSelect(value);
919
1230
  },
920
1231
  children: [
921
- /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("div", { className: "relative flex-1", children: [
922
- /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_lucide_react4.Link2, { className: "pointer-events-none absolute top-1/2 left-2.5 size-4 -translate-y-1/2 text-panel-muted-fg" }),
923
- /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
1232
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { className: "relative flex-1", children: [
1233
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_lucide_react4.Link2, { className: "pointer-events-none absolute top-1/2 left-2.5 size-4 -translate-y-1/2 text-panel-muted-fg" }),
1234
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
924
1235
  Input,
925
1236
  {
926
1237
  value: url,
@@ -930,7 +1241,7 @@ function ImagePicker({
930
1241
  }
931
1242
  )
932
1243
  ] }),
933
- /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
1244
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
934
1245
  Button,
935
1246
  {
936
1247
  type: "submit",
@@ -944,22 +1255,22 @@ function ImagePicker({
944
1255
  ]
945
1256
  }
946
1257
  ),
947
- library && /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("section", { children: [
948
- /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("h3", { className: "mb-2 text-xs font-medium tracking-wide text-panel-muted-fg uppercase", children: labels.imagePickerGallery }),
949
- loading && images.length === 0 && pending === 0 ? /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("div", { className: "flex justify-center py-8", children: /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(Spinner, {}) }) : images.length === 0 && pending === 0 ? /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("p", { className: "py-6 text-center text-sm text-panel-muted-fg", children: labels.imagePickerEmpty }) : /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("ul", { className: "grid grid-cols-3 gap-3 sm:grid-cols-4", children: [
950
- Array.from({ length: pending }, (_, i) => /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
1258
+ library && /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("section", { children: [
1259
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("h3", { className: "mb-2 text-xs font-medium tracking-wide text-panel-muted-fg uppercase", children: labels.imagePickerGallery }),
1260
+ loading && images.length === 0 && pending === 0 ? /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("div", { className: "flex justify-center py-8", children: /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(Spinner, {}) }) : images.length === 0 && pending === 0 ? /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("p", { className: "py-6 text-center text-sm text-panel-muted-fg", children: labels.imagePickerEmpty }) : /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("ul", { className: "grid grid-cols-3 gap-3 sm:grid-cols-4", children: [
1261
+ Array.from({ length: pending }, (_, i) => /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
951
1262
  "li",
952
1263
  {
953
1264
  className: "flex aspect-[4/3] items-center justify-center rounded-lg border border-panel-border bg-panel-elevated",
954
- children: /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(Spinner, {})
1265
+ children: /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(Spinner, {})
955
1266
  },
956
1267
  `pending-${i}`
957
1268
  )),
958
1269
  images.map((image) => {
959
1270
  const selected = image.url === currentUrl;
960
1271
  const confirming = confirmId === image.id;
961
- return /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("li", { className: "group relative", children: [
962
- /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
1272
+ return /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("li", { className: "group relative", children: [
1273
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
963
1274
  "button",
964
1275
  {
965
1276
  type: "button",
@@ -969,7 +1280,7 @@ function ImagePicker({
969
1280
  "block aspect-[4/3] w-full overflow-hidden rounded-lg border bg-black/25 transition outline-none focus-visible:ring-2 focus-visible:ring-brand",
970
1281
  selected ? "border-brand ring-2 ring-brand" : "border-panel-border hover:border-panel-muted-fg"
971
1282
  ),
972
- children: /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
1283
+ children: /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
973
1284
  "img",
974
1285
  {
975
1286
  src: image.url,
@@ -980,9 +1291,9 @@ function ImagePicker({
980
1291
  )
981
1292
  }
982
1293
  ),
983
- selected && /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("span", { className: "pointer-events-none absolute top-1.5 left-1.5 flex size-5 items-center justify-center rounded-full bg-brand text-brand-fg", children: /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_lucide_react4.Check, { className: "size-3" }) }),
984
- image.name && /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("span", { className: "pointer-events-none absolute inset-x-0 bottom-0 truncate rounded-b-lg bg-black/60 px-2 py-1 text-[11px] text-white opacity-0 transition group-hover:opacity-100", children: image.name }),
985
- library.remove && /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(
1294
+ selected && /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("span", { className: "pointer-events-none absolute top-1.5 left-1.5 flex size-5 items-center justify-center rounded-full bg-brand text-brand-fg", children: /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_lucide_react4.Check, { className: "size-3" }) }),
1295
+ image.name && /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("span", { className: "pointer-events-none absolute inset-x-0 bottom-0 truncate rounded-b-lg bg-black/60 px-2 py-1 text-[11px] text-white opacity-0 transition group-hover:opacity-100", children: image.name }),
1296
+ library.remove && /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(
986
1297
  "button",
987
1298
  {
988
1299
  type: "button",
@@ -995,7 +1306,7 @@ function ImagePicker({
995
1306
  confirming ? "bg-red-600 opacity-100" : "bg-black/60 opacity-0 group-hover:opacity-100 hover:bg-black/80"
996
1307
  ),
997
1308
  children: [
998
- /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_lucide_react4.Trash2, { className: "size-3.5" }),
1309
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_lucide_react4.Trash2, { className: "size-3.5" }),
999
1310
  confirming && labels.imagePickerConfirmDelete
1000
1311
  ]
1001
1312
  }
@@ -1017,171 +1328,1201 @@ function matchesAccept(file, accept) {
1017
1328
  });
1018
1329
  }
1019
1330
 
1331
+ // src/components/editor/inspector/Inspector.tsx
1332
+ var import_react7 = require("react");
1333
+ var import_react8 = require("@grapesjs/react");
1334
+ var import_lucide_react9 = require("lucide-react");
1335
+
1020
1336
  // src/components/editor/ColorField.tsx
1021
- var import_react5 = require("react");
1337
+ var import_react6 = require("react");
1022
1338
  var import_react_colorful = require("react-colorful");
1339
+ var import_lucide_react6 = require("lucide-react");
1340
+
1341
+ // src/components/editor/inspector/controls.tsx
1342
+ var import_react5 = require("react");
1023
1343
  var import_lucide_react5 = require("lucide-react");
1024
- var import_jsx_runtime11 = require("react/jsx-runtime");
1025
- var PRESETS = [
1026
- "#000000",
1027
- "#374151",
1028
- "#6b7280",
1029
- "#d1d5db",
1030
- "#f3f4f6",
1031
- "#ffffff",
1032
- "#dc2626",
1033
- "#ea580c",
1034
- "#ca8a04",
1035
- "#16a34a",
1036
- "#0284c7",
1037
- "#4f46e5",
1038
- "#9333ea",
1039
- "#db2777"
1040
- ];
1041
- var eyeDropper = () => globalThis.EyeDropper;
1042
- function ColorField({
1043
- value,
1044
- placeholder,
1045
- documentColors,
1344
+
1345
+ // src/components/editor/inspector/values.ts
1346
+ var import_core2 = require("@mjml-agent-editor/core");
1347
+ var SIDES = ["top", "right", "bottom", "left"];
1348
+ function expandBox(value) {
1349
+ const parts = value.trim().split(/\s+/).filter(Boolean);
1350
+ if (parts.length === 0 || parts.length > 4) return null;
1351
+ const [t, r = t, b = t, l = r] = parts;
1352
+ return [t, r, b, l];
1353
+ }
1354
+ function readBox(attributes, attr) {
1355
+ const short = expandBox(attributes[attr] ?? "") ?? ["", "", "", ""];
1356
+ return SIDES.map((side, i) => attributes[`${attr}-${side}`] ?? short[i]);
1357
+ }
1358
+ function boxShorthand(sides) {
1359
+ const [t, r, b, l] = sides.map((s) => s.trim() || "0");
1360
+ if (t === b && r === l) return t === r ? t : `${t} ${r}`;
1361
+ if (r === l) return `${t} ${r} ${b}`;
1362
+ return `${t} ${r} ${b} ${l}`;
1363
+ }
1364
+ function parseLength(value, units) {
1365
+ const match = /^(-?\d*\.?\d+)\s*(px|%|em)?$/.exec(value.trim());
1366
+ if (!match) return null;
1367
+ const unit = match[2] ?? "";
1368
+ return units.includes(unit) ? { amount: match[1], unit } : null;
1369
+ }
1370
+ var NO_SHARE = "-noshare";
1371
+ function parseNetwork(name) {
1372
+ return name.endsWith(NO_SHARE) ? { network: name.slice(0, -NO_SHARE.length), share: false } : { network: name, share: name !== "custom" && name !== "" };
1373
+ }
1374
+ function networkName(network, share) {
1375
+ return share || network === "custom" ? network : `${network}${NO_SHARE}`;
1376
+ }
1377
+
1378
+ // src/components/editor/inspector/controls.tsx
1379
+ var import_jsx_runtime12 = require("react/jsx-runtime");
1380
+ var FIELD = "h-7 w-full min-w-0 rounded-md border border-panel-border bg-panel-elevated px-2 text-xs text-panel-fg outline-none placeholder:text-panel-muted-fg/70 focus:border-brand";
1381
+ var FieldLabel = (0, import_react5.createContext)({});
1382
+ var useFieldLabel = () => {
1383
+ const { label, help } = (0, import_react5.useContext)(FieldLabel);
1384
+ return { "aria-labelledby": label, "aria-describedby": help };
1385
+ };
1386
+ function Row({
1387
+ label,
1388
+ help,
1389
+ own,
1390
+ wide,
1391
+ inline,
1046
1392
  labels,
1047
- onChange
1393
+ onReset,
1394
+ children
1048
1395
  }) {
1049
- const [text, setText] = (0, import_react5.useState)(value);
1050
- const [open, setOpen] = (0, import_react5.useState)(false);
1051
- const [used, setUsed] = (0, import_react5.useState)([]);
1052
- const pressed = (0, import_react5.useRef)(false);
1053
- const dragged = (0, import_react5.useRef)(null);
1054
- const fieldRef = (0, import_react5.useRef)(null);
1055
- const anchor = (0, import_react5.useRef)({
1056
- getBoundingClientRect: () => {
1057
- const field = fieldRef.current?.getBoundingClientRect() ?? new DOMRect();
1058
- const panel = fieldRef.current?.closest(".editor-dark")?.getBoundingClientRect();
1059
- return new DOMRect(panel?.right ?? field.right, field.top, 0, field.height);
1396
+ const id = (0, import_react5.useId)();
1397
+ const helpId = (0, import_react5.useId)();
1398
+ const [helpOpen, setHelpOpen] = (0, import_react5.useState)(false);
1399
+ const control = /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(FieldLabel.Provider, { value: { label: id, help: help ? helpId : void 0 }, children });
1400
+ const caption = /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("span", { className: "flex min-w-0 items-center gap-1.5 text-[11px] text-panel-muted-fg", children: [
1401
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
1402
+ "span",
1403
+ {
1404
+ "aria-hidden": true,
1405
+ className: cn("size-1.5 shrink-0 rounded-full", own ? "bg-brand" : "bg-transparent")
1406
+ }
1407
+ ),
1408
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("span", { id, className: "truncate", children: label }),
1409
+ help && // Its own provider: a host need not wrap the editor in one, as only EditorHeader asked.
1410
+ // Opened by a tap as well: Radix opens a tooltip on hover and focus, not on touch.
1411
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(TooltipProvider, { children: /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(Tooltip, { open: helpOpen, onOpenChange: setHelpOpen, children: [
1412
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(TooltipTrigger, { asChild: true, children: /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
1413
+ "button",
1414
+ {
1415
+ type: "button",
1416
+ "aria-label": help,
1417
+ onClick: () => setHelpOpen((open) => !open),
1418
+ className: "-m-0.5 shrink-0 rounded p-0.5 text-panel-muted-fg/70 hover:text-panel-fg focus-visible:text-panel-fg",
1419
+ children: /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(import_lucide_react5.CircleHelp, { className: "size-3" })
1420
+ }
1421
+ ) }),
1422
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(TooltipContent, { side: "top", className: "max-w-56 leading-snug", children: help })
1423
+ ] }) }),
1424
+ own && /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
1425
+ "button",
1426
+ {
1427
+ type: "button",
1428
+ onClick: onReset,
1429
+ title: labels.resetToDefault,
1430
+ "aria-label": `${labels.resetToDefault}: ${label}`,
1431
+ className: "ml-auto rounded p-0.5 text-panel-muted-fg opacity-0 group-hover/row:opacity-100 hover:text-panel-fg focus-visible:opacity-100",
1432
+ children: /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(import_lucide_react5.RotateCcw, { className: "size-3" })
1433
+ }
1434
+ )
1435
+ ] });
1436
+ return /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { className: "group/row px-3 py-1", children: [
1437
+ wide ? /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { className: "flex flex-col gap-1", children: [
1438
+ caption,
1439
+ control
1440
+ ] }) : inline ? /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { className: "flex items-center justify-between gap-2", children: [
1441
+ caption,
1442
+ control
1443
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { className: "grid grid-cols-[92px_minmax(0,1fr)] items-center gap-2", children: [
1444
+ caption,
1445
+ control
1446
+ ] }),
1447
+ help && /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("span", { id: helpId, hidden: true, children: help })
1448
+ ] });
1449
+ }
1450
+ function useDraft(value, commit, source = value) {
1451
+ const [draft, setDraft] = (0, import_react5.useState)(value);
1452
+ (0, import_react5.useEffect)(() => setDraft(value), [value, source]);
1453
+ const done = () => {
1454
+ if (draft.trim() !== value) commit(draft.trim());
1455
+ };
1456
+ const onKeyDown = (e) => {
1457
+ if (e.key === "Enter") e.currentTarget.blur();
1458
+ if (e.key === "Escape") {
1459
+ setDraft(value);
1460
+ requestAnimationFrame(() => e.target.blur());
1060
1461
  }
1462
+ };
1463
+ return { draft, setDraft, done, onKeyDown };
1464
+ }
1465
+ function TextInput({
1466
+ value,
1467
+ placeholder,
1468
+ accept,
1469
+ onCommit
1470
+ }) {
1471
+ const [invalid, setInvalid] = (0, import_react5.useState)(false);
1472
+ const field = useFieldLabel();
1473
+ const { draft, setDraft, done, onKeyDown } = useDraft(value, (next) => {
1474
+ const ok = !accept || accept(next);
1475
+ setInvalid(!ok);
1476
+ if (ok) onCommit(next);
1061
1477
  });
1062
- (0, import_react5.useEffect)(() => setText(value), [value]);
1063
- const latestOnChange = (0, import_react5.useRef)(onChange);
1064
- latestOnChange.current = onChange;
1065
- const release = (0, import_react5.useCallback)(() => {
1066
- pressed.current = false;
1067
- if (dragged.current !== null) latestOnChange.current(dragged.current, false);
1068
- dragged.current = null;
1069
- }, []);
1070
- (0, import_react5.useEffect)(() => {
1071
- document.addEventListener("lostpointercapture", release);
1072
- return () => {
1073
- document.removeEventListener("lostpointercapture", release);
1074
- release();
1075
- };
1076
- }, [release]);
1077
- const hex = toHex(value) ?? toHex(placeholder ?? "") ?? "#000000";
1078
- const translucent = hex.length === 9;
1079
- const empty = !value;
1080
- const commit = (next) => {
1081
- dragged.current = null;
1082
- onChange(next, false);
1478
+ return /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
1479
+ "input",
1480
+ {
1481
+ className: cn(FIELD, invalid && "border-red-400"),
1482
+ ...field,
1483
+ "aria-invalid": invalid,
1484
+ value: draft,
1485
+ placeholder,
1486
+ onChange: (e) => setDraft(e.target.value),
1487
+ onBlur: done,
1488
+ onKeyDown
1489
+ }
1490
+ );
1491
+ }
1492
+ var BARE_NUMBER = /^-?\d*\.?\d+$/;
1493
+ function normaliseLength(text, units, negative = false, unit = units[0]) {
1494
+ if (text === "") return "";
1495
+ const parsed = BARE_NUMBER.test(text) ? { amount: text, unit } : parseLength(text, units);
1496
+ if (!parsed || !negative && parsed.amount.startsWith("-")) return null;
1497
+ return `${parsed.amount}${parsed.unit}`;
1498
+ }
1499
+ function stepAmount(amount, delta, negative = false) {
1500
+ const moved = Math.round((Number(amount) + delta) * 100) / 100;
1501
+ const next = String(negative ? moved : Math.max(0, moved));
1502
+ return BARE_NUMBER.test(next) ? next : amount;
1503
+ }
1504
+ var REPEAT_AFTER_MS = 400;
1505
+ var REPEAT_EVERY_MS = 60;
1506
+ function LengthInput({
1507
+ value,
1508
+ placeholder,
1509
+ units,
1510
+ negative = false,
1511
+ compact = false,
1512
+ labelledBy,
1513
+ labels,
1514
+ onCommit
1515
+ }) {
1516
+ const fieldLabel = useFieldLabel();
1517
+ const own = parseLength(value, units);
1518
+ const fallback = parseLength(placeholder ?? "", units);
1519
+ const [picked, setPicked] = (0, import_react5.useState)(null);
1520
+ const unit = own?.unit ?? picked ?? fallback?.unit ?? units[0];
1521
+ const [invalid, setInvalid] = (0, import_react5.useState)(false);
1522
+ const commit = (0, import_react5.useRef)(onCommit);
1523
+ commit.current = onCommit;
1524
+ const endHold = (0, import_react5.useRef)(null);
1525
+ (0, import_react5.useEffect)(() => () => endHold.current?.(), []);
1526
+ const { draft, setDraft, done, onKeyDown } = useDraft(
1527
+ own ? own.amount : value,
1528
+ (next) => {
1529
+ endHold.current?.();
1530
+ const normal = normaliseLength(next, units, negative, unit);
1531
+ setInvalid(normal === null);
1532
+ if (normal !== null) commit.current(normal);
1533
+ },
1534
+ value
1535
+ );
1536
+ const from = () => {
1537
+ const text = draft.trim();
1538
+ if (BARE_NUMBER.test(text)) return { amount: text, unit };
1539
+ if (text === "") return { amount: fallback?.amount ?? "0", unit };
1540
+ return parseLength(text, units);
1083
1541
  };
1084
- const drag = (next) => {
1085
- setText(next);
1086
- if (!pressed.current) return commit(next);
1087
- dragged.current = next;
1088
- onChange(next, true);
1542
+ const stepOnce = (delta) => {
1543
+ endHold.current?.();
1544
+ const start = from();
1545
+ if (!start) return setInvalid(true);
1546
+ const amount = stepAmount(start.amount, delta, negative);
1547
+ setInvalid(false);
1548
+ setDraft(amount);
1549
+ commit.current(`${amount}${start.unit}`);
1089
1550
  };
1090
- const pickFromScreen = async () => {
1091
- const Ctor = eyeDropper();
1092
- if (!Ctor) return;
1551
+ const onKey = (e) => {
1552
+ if (e.key !== "ArrowUp" && e.key !== "ArrowDown") return onKeyDown(e);
1553
+ e.preventDefault();
1554
+ stepOnce((e.key === "ArrowUp" ? 1 : -1) * (e.shiftKey ? 10 : 1));
1555
+ };
1556
+ const press = (direction) => (e) => {
1557
+ if (e.button !== 0 || endHold.current) return;
1558
+ e.preventDefault();
1559
+ const start = from();
1560
+ if (!start) return setInvalid(true);
1561
+ const delta = direction * (e.shiftKey ? 10 : 1);
1562
+ let amount = stepAmount(start.amount, delta, negative);
1563
+ const show = () => {
1564
+ setInvalid(false);
1565
+ setDraft(amount);
1566
+ commit.current(`${amount}${start.unit}`, true);
1567
+ };
1568
+ show();
1569
+ let timer = window.setTimeout(function repeat() {
1570
+ amount = stepAmount(amount, delta, negative);
1571
+ show();
1572
+ timer = window.setTimeout(repeat, REPEAT_EVERY_MS);
1573
+ }, REPEAT_AFTER_MS);
1574
+ const button = e.currentTarget;
1093
1575
  try {
1094
- const { sRGBHex } = await new Ctor().open();
1095
- commit(sRGBHex);
1576
+ button.setPointerCapture(e.pointerId);
1096
1577
  } catch {
1097
1578
  }
1579
+ const events = ["pointerup", "pointercancel", "lostpointercapture"];
1580
+ const pointerId = e.pointerId;
1581
+ const end = () => {
1582
+ window.clearTimeout(timer);
1583
+ for (const type of events) button.removeEventListener(type, release);
1584
+ window.removeEventListener("pointerup", release);
1585
+ window.removeEventListener("blur", end);
1586
+ endHold.current = null;
1587
+ commit.current(`${amount}${start.unit}`);
1588
+ };
1589
+ const release = (ev) => {
1590
+ if (ev.pointerId === void 0 || ev.pointerId === pointerId)
1591
+ end();
1592
+ };
1593
+ endHold.current = end;
1594
+ for (const type of events) button.addEventListener(type, release);
1595
+ window.addEventListener("pointerup", release);
1596
+ window.addEventListener("blur", end);
1098
1597
  };
1099
- return /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(
1100
- Popover,
1598
+ const click = (direction) => (e) => {
1599
+ if (e.detail === 0) stepOnce(direction);
1600
+ };
1601
+ const pickUnit = () => {
1602
+ endHold.current?.();
1603
+ const next = units[(units.indexOf(unit) + 1) % units.length];
1604
+ const text = draft.trim();
1605
+ if (!BARE_NUMBER.test(text)) return setPicked(next);
1606
+ const normal = normaliseLength(text, units, negative, next);
1607
+ if (normal === null) return setInvalid(true);
1608
+ setPicked(next);
1609
+ commit.current(normal);
1610
+ };
1611
+ const unitName = (u) => u || labels.noUnit;
1612
+ const readable = draft.trim() === "" || BARE_NUMBER.test(draft.trim());
1613
+ const arrow = "flex h-1/2 w-3.5 items-center justify-center text-panel-muted-fg hover:bg-panel-hover hover:text-panel-fg";
1614
+ return /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(
1615
+ "div",
1101
1616
  {
1102
- open,
1103
- onOpenChange: (next) => {
1104
- if (next) setUsed(documentColors());
1105
- else if (text.trim() !== value) commit(text.trim());
1106
- setOpen(next);
1107
- },
1617
+ className: cn(
1618
+ "group/length flex h-7 w-full min-w-0 items-center overflow-hidden rounded-md border border-panel-border bg-panel-elevated focus-within:border-brand",
1619
+ invalid && "border-red-400"
1620
+ ),
1108
1621
  children: [
1109
- /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(PopoverAnchor, { virtualRef: anchor }),
1110
- /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(
1111
- "div",
1622
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
1623
+ "input",
1112
1624
  {
1113
- ref: fieldRef,
1114
- className: "flex h-8 w-full items-center gap-1.5 rounded-md border border-panel-border bg-panel-elevated pr-1.5 pl-2 focus-within:border-brand",
1625
+ className: "h-full w-full min-w-0 flex-1 bg-transparent pl-2 font-mono text-xs text-panel-fg outline-none placeholder:text-panel-muted-fg/70",
1626
+ value: draft,
1627
+ placeholder: fallback ? fallback.amount : placeholder,
1628
+ inputMode: "decimal",
1629
+ ...fieldLabel,
1630
+ "aria-labelledby": labelledBy ?? fieldLabel["aria-labelledby"],
1631
+ "aria-invalid": invalid,
1632
+ onChange: (e) => {
1633
+ endHold.current?.();
1634
+ setDraft(e.target.value);
1635
+ },
1636
+ onBlur: done,
1637
+ onKeyDown: onKey
1638
+ }
1639
+ ),
1640
+ readable && (units.length > 1 && !compact ? /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
1641
+ "button",
1642
+ {
1643
+ type: "button",
1644
+ onClick: pickUnit,
1645
+ title: labels.changeUnit(unitName(unit)),
1646
+ "aria-label": labels.changeUnit(unitName(unit)),
1647
+ className: "shrink-0 rounded px-0.5 font-mono text-[10px] text-panel-muted-fg underline decoration-dotted underline-offset-2 hover:text-panel-fg",
1648
+ children: unitName(unit)
1649
+ }
1650
+ ) : (
1651
+ // Narrow, the unit gives its place to the arrows while they show; a unit is typed.
1652
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
1653
+ "span",
1654
+ {
1655
+ "aria-hidden": true,
1656
+ className: cn(
1657
+ "shrink-0 px-0.5 font-mono text-[10px] text-panel-muted-fg",
1658
+ compact && "group-focus-within/length:hidden group-hover/length:hidden"
1659
+ ),
1660
+ children: unitName(unit)
1661
+ }
1662
+ )
1663
+ )),
1664
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(
1665
+ "span",
1666
+ {
1667
+ className: cn(
1668
+ "h-full shrink-0 flex-col border-l border-panel-border",
1669
+ compact ? "hidden group-focus-within/length:flex group-hover/length:flex" : "flex"
1670
+ ),
1115
1671
  children: [
1116
- /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
1117
- "input",
1672
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
1673
+ "button",
1118
1674
  {
1119
- value: text,
1120
- placeholder,
1121
- spellCheck: false,
1122
- onChange: (e) => setText(e.target.value),
1123
- onBlur: () => text !== value && commit(text.trim()),
1124
- onKeyDown: (e) => {
1125
- if (e.key === "Enter") commit(text.trim());
1126
- if (e.key === "Escape") setText(value);
1127
- },
1128
- className: "min-w-0 flex-1 bg-transparent font-mono text-xs text-panel-fg outline-none placeholder:text-panel-muted-fg"
1675
+ type: "button",
1676
+ tabIndex: -1,
1677
+ "aria-label": labels.increase,
1678
+ onPointerDown: press(1),
1679
+ onClick: click(1),
1680
+ className: arrow,
1681
+ children: /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(import_lucide_react5.ChevronUp, { className: "size-2.5" })
1129
1682
  }
1130
1683
  ),
1131
- /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(PopoverTrigger, { asChild: true, children: /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(
1684
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
1132
1685
  "button",
1133
1686
  {
1134
1687
  type: "button",
1135
- title: labels.colorPickerOpen,
1136
- "aria-label": labels.colorPickerOpen,
1137
- className: "relative size-5 shrink-0 overflow-hidden rounded border border-white/20 outline-none focus-visible:ring-2 focus-visible:ring-brand",
1138
- children: [
1139
- /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(Checkerboard, {}),
1140
- !empty && /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("span", { className: "absolute inset-0", style: { background: value } })
1141
- ]
1688
+ tabIndex: -1,
1689
+ "aria-label": labels.decrease,
1690
+ onPointerDown: press(-1),
1691
+ onClick: click(-1),
1692
+ className: arrow,
1693
+ children: /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(import_lucide_react5.ChevronDown, { className: "size-2.5" })
1142
1694
  }
1143
- ) })
1695
+ )
1144
1696
  ]
1145
1697
  }
1146
- ),
1147
- /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(
1148
- PopoverContent,
1149
- {
1150
- side: "right",
1151
- align: "start",
1152
- sideOffset: 8,
1153
- className: "editor-dark flex w-52 flex-col gap-2.5 border border-panel-border bg-panel p-2.5 text-panel-fg",
1154
- children: [
1155
- /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
1156
- "div",
1698
+ )
1699
+ ]
1700
+ }
1701
+ );
1702
+ }
1703
+ function BoxInput({
1704
+ attributes,
1705
+ attr,
1706
+ placeholder,
1707
+ units,
1708
+ labels,
1709
+ onCommit
1710
+ }) {
1711
+ const sides = readBox(attributes, attr);
1712
+ const defaults = readBox(placeholder ? { [attr]: placeholder } : {}, attr);
1713
+ const symmetric = sides[0] === sides[2] && sides[1] === sides[3];
1714
+ const [each, setEach] = (0, import_react5.useState)(!symmetric);
1715
+ (0, import_react5.useEffect)(() => {
1716
+ if (!symmetric) setEach(true);
1717
+ }, [symmetric]);
1718
+ const set = (indices, v, partial) => {
1719
+ const next = sides.map((s, i) => s || defaults[i]);
1720
+ for (const i of indices) next[i] = v || defaults[i];
1721
+ onCommit(next.every((s, i) => s === defaults[i]) ? "" : boxShorthand(next), partial);
1722
+ };
1723
+ const fieldLabel = useFieldLabel();
1724
+ const sideId = (0, import_react5.useId)();
1725
+ const sideNames = [labels.sideTop, labels.sideRight, labels.sideBottom, labels.sideLeft];
1726
+ const pairs = each ? SIDES.map((_, i) => [sideNames[i], [i]]) : [
1727
+ [labels.sidesVertical, [0, 2]],
1728
+ [labels.sidesHorizontal, [1, 3]]
1729
+ ];
1730
+ return /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { className: "flex items-start gap-1.5", children: [
1731
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("div", { className: cn("grid flex-1 gap-1.5", each ? "grid-cols-4" : "grid-cols-2"), children: pairs.map(([name, indices]) => /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("label", { className: "flex min-w-0 flex-col gap-0.5", title: name, children: [
1732
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
1733
+ LengthInput,
1734
+ {
1735
+ value: sides[indices[0]],
1736
+ placeholder: defaults[indices[0]] || "0",
1737
+ units,
1738
+ compact: each,
1739
+ labels,
1740
+ labelledBy: [fieldLabel["aria-labelledby"], `${sideId}-${indices[0]}`].filter(Boolean).join(" "),
1741
+ onCommit: (v, partial) => set(indices, v, partial)
1742
+ }
1743
+ ),
1744
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
1745
+ "span",
1746
+ {
1747
+ id: `${sideId}-${indices[0]}`,
1748
+ className: "truncate text-center text-[10px] text-panel-muted-fg/80",
1749
+ children: name
1750
+ }
1751
+ )
1752
+ ] }, name)) }),
1753
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
1754
+ "button",
1755
+ {
1756
+ type: "button",
1757
+ onClick: () => setEach((v) => !v),
1758
+ disabled: each && !symmetric,
1759
+ "aria-pressed": each,
1760
+ title: labels.editEachSide,
1761
+ "aria-label": labels.editEachSide,
1762
+ className: cn(
1763
+ "flex size-7 shrink-0 items-center justify-center rounded-md border border-panel-border text-panel-muted-fg hover:text-panel-fg disabled:opacity-40",
1764
+ each && "border-brand text-panel-fg"
1765
+ ),
1766
+ children: /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(import_lucide_react5.Grid2x2, { className: "size-3.5" })
1767
+ }
1768
+ )
1769
+ ] });
1770
+ }
1771
+ var ALIGN_ICONS = {
1772
+ left: /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(import_lucide_react5.AlignLeft, { className: "size-3.5" }),
1773
+ center: /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(import_lucide_react5.AlignCenter, { className: "size-3.5" }),
1774
+ right: /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(import_lucide_react5.AlignRight, { className: "size-3.5" }),
1775
+ justify: /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(import_lucide_react5.AlignJustify, { className: "size-3.5" })
1776
+ };
1777
+ function Segmented({
1778
+ value,
1779
+ fallback,
1780
+ options,
1781
+ name,
1782
+ onCommit
1783
+ }) {
1784
+ const current = value || fallback;
1785
+ const iconsOnly = options.every((o) => o.value in ALIGN_ICONS);
1786
+ return /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("div", { className: "flex h-7 overflow-hidden rounded-md border border-panel-border bg-panel-elevated", children: options.map((o) => {
1787
+ const active = current === o.value;
1788
+ return /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
1789
+ "button",
1790
+ {
1791
+ type: "button",
1792
+ "aria-pressed": active,
1793
+ title: name(o),
1794
+ "aria-label": name(o),
1795
+ onClick: () => o.value !== value && onCommit(o.value),
1796
+ className: cn(
1797
+ "flex min-w-0 flex-1 items-center justify-center px-1 text-[11px] text-panel-muted-fg transition-colors hover:text-panel-fg",
1798
+ active && (value ? "bg-brand text-brand-fg hover:text-brand-fg" : "bg-panel-hover text-panel-fg")
1799
+ ),
1800
+ children: iconsOnly ? ALIGN_ICONS[o.value] : /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("span", { className: "truncate", children: name(o) })
1801
+ },
1802
+ o.value
1803
+ );
1804
+ }) });
1805
+ }
1806
+ function Select({
1807
+ value,
1808
+ fallback,
1809
+ options,
1810
+ name,
1811
+ onCommit
1812
+ }) {
1813
+ const known = !value || options.some((o) => o.value === value);
1814
+ const fallbackOption = options.find((o) => o.value === fallback);
1815
+ const field = useFieldLabel();
1816
+ return /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(
1817
+ "select",
1818
+ {
1819
+ className: cn(FIELD, "px-1.5"),
1820
+ ...field,
1821
+ value: value || "",
1822
+ onChange: (e) => onCommit(e.target.value),
1823
+ children: [
1824
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("option", { value: "", children: fallbackOption ? name(fallbackOption) : "\u2014" }),
1825
+ !known && /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("option", { value, children: value }),
1826
+ options.map((o) => /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("option", { value: o.value, children: name(o) }, o.value))
1827
+ ]
1828
+ }
1829
+ );
1830
+ }
1831
+ function Toggle({
1832
+ checked,
1833
+ label,
1834
+ onCommit
1835
+ }) {
1836
+ return /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
1837
+ Switch,
1838
+ {
1839
+ checked,
1840
+ onCheckedChange: onCommit,
1841
+ "aria-label": label,
1842
+ className: "justify-self-end data-checked:bg-brand data-unchecked:bg-panel-border"
1843
+ }
1844
+ );
1845
+ }
1846
+
1847
+ // src/components/editor/mjml-attributes.ts
1848
+ var ALLOWED_ATTRIBUTES = {
1849
+ "mj-body": [
1850
+ "background-color",
1851
+ "width"
1852
+ ],
1853
+ "mj-wrapper": [
1854
+ "background-color",
1855
+ "background-position",
1856
+ "background-position-x",
1857
+ "background-position-y",
1858
+ "background-repeat",
1859
+ "background-size",
1860
+ "background-url",
1861
+ "border",
1862
+ "border-bottom",
1863
+ "border-left",
1864
+ "border-radius",
1865
+ "border-right",
1866
+ "border-top",
1867
+ "direction",
1868
+ "full-width",
1869
+ "gap",
1870
+ "padding",
1871
+ "padding-bottom",
1872
+ "padding-left",
1873
+ "padding-right",
1874
+ "padding-top",
1875
+ "text-align",
1876
+ "text-padding"
1877
+ ],
1878
+ "mj-section": [
1879
+ "background-color",
1880
+ "background-position",
1881
+ "background-position-x",
1882
+ "background-position-y",
1883
+ "background-repeat",
1884
+ "background-size",
1885
+ "background-url",
1886
+ "border",
1887
+ "border-bottom",
1888
+ "border-left",
1889
+ "border-radius",
1890
+ "border-right",
1891
+ "border-top",
1892
+ "direction",
1893
+ "full-width",
1894
+ "padding",
1895
+ "padding-bottom",
1896
+ "padding-left",
1897
+ "padding-right",
1898
+ "padding-top",
1899
+ "text-align",
1900
+ "text-padding"
1901
+ ],
1902
+ "mj-column": [
1903
+ "background-color",
1904
+ "border",
1905
+ "border-bottom",
1906
+ "border-left",
1907
+ "border-radius",
1908
+ "border-right",
1909
+ "border-top",
1910
+ "direction",
1911
+ "inner-background-color",
1912
+ "inner-border",
1913
+ "inner-border-bottom",
1914
+ "inner-border-left",
1915
+ "inner-border-radius",
1916
+ "inner-border-right",
1917
+ "inner-border-top",
1918
+ "padding",
1919
+ "padding-bottom",
1920
+ "padding-left",
1921
+ "padding-right",
1922
+ "padding-top",
1923
+ "vertical-align",
1924
+ "width"
1925
+ ],
1926
+ "mj-group": [
1927
+ "background-color",
1928
+ "direction",
1929
+ "vertical-align",
1930
+ "width"
1931
+ ],
1932
+ "mj-text": [
1933
+ "align",
1934
+ "background-color",
1935
+ "color",
1936
+ "container-background-color",
1937
+ "font-family",
1938
+ "font-size",
1939
+ "font-style",
1940
+ "font-weight",
1941
+ "height",
1942
+ "letter-spacing",
1943
+ "line-height",
1944
+ "padding",
1945
+ "padding-bottom",
1946
+ "padding-left",
1947
+ "padding-right",
1948
+ "padding-top",
1949
+ "text-decoration",
1950
+ "text-transform",
1951
+ "vertical-align"
1952
+ ],
1953
+ "mj-button": [
1954
+ "align",
1955
+ "background-color",
1956
+ "border",
1957
+ "border-bottom",
1958
+ "border-left",
1959
+ "border-radius",
1960
+ "border-right",
1961
+ "border-top",
1962
+ "color",
1963
+ "container-background-color",
1964
+ "font-family",
1965
+ "font-size",
1966
+ "font-style",
1967
+ "font-weight",
1968
+ "height",
1969
+ "href",
1970
+ "inner-padding",
1971
+ "letter-spacing",
1972
+ "line-height",
1973
+ "name",
1974
+ "padding",
1975
+ "padding-bottom",
1976
+ "padding-left",
1977
+ "padding-right",
1978
+ "padding-top",
1979
+ "rel",
1980
+ "target",
1981
+ "text-align",
1982
+ "text-decoration",
1983
+ "text-transform",
1984
+ "title",
1985
+ "vertical-align",
1986
+ "width"
1987
+ ],
1988
+ "mj-image": [
1989
+ "align",
1990
+ "alt",
1991
+ "border",
1992
+ "border-bottom",
1993
+ "border-left",
1994
+ "border-radius",
1995
+ "border-right",
1996
+ "border-top",
1997
+ "container-background-color",
1998
+ "fluid-on-mobile",
1999
+ "font-size",
2000
+ "height",
2001
+ "href",
2002
+ "max-height",
2003
+ "name",
2004
+ "padding",
2005
+ "padding-bottom",
2006
+ "padding-left",
2007
+ "padding-right",
2008
+ "padding-top",
2009
+ "rel",
2010
+ "sizes",
2011
+ "src",
2012
+ "srcset",
2013
+ "target",
2014
+ "title",
2015
+ "usemap",
2016
+ "width"
2017
+ ],
2018
+ "mj-divider": [
2019
+ "align",
2020
+ "border-color",
2021
+ "border-style",
2022
+ "border-width",
2023
+ "container-background-color",
2024
+ "padding",
2025
+ "padding-bottom",
2026
+ "padding-left",
2027
+ "padding-right",
2028
+ "padding-top",
2029
+ "width"
2030
+ ],
2031
+ "mj-spacer": [
2032
+ "border",
2033
+ "border-bottom",
2034
+ "border-left",
2035
+ "border-right",
2036
+ "border-top",
2037
+ "container-background-color",
2038
+ "height",
2039
+ "padding",
2040
+ "padding-bottom",
2041
+ "padding-left",
2042
+ "padding-right",
2043
+ "padding-top"
2044
+ ],
2045
+ "mj-social": [
2046
+ "align",
2047
+ "border-radius",
2048
+ "color",
2049
+ "container-background-color",
2050
+ "font-family",
2051
+ "font-size",
2052
+ "font-style",
2053
+ "font-weight",
2054
+ "icon-height",
2055
+ "icon-padding",
2056
+ "icon-size",
2057
+ "inner-padding",
2058
+ "line-height",
2059
+ "mode",
2060
+ "padding",
2061
+ "padding-bottom",
2062
+ "padding-left",
2063
+ "padding-right",
2064
+ "padding-top",
2065
+ "table-layout",
2066
+ "text-decoration",
2067
+ "text-padding",
2068
+ "vertical-align"
2069
+ ],
2070
+ "mj-social-element": [
2071
+ "align",
2072
+ "alt",
2073
+ "background-color",
2074
+ "border-radius",
2075
+ "color",
2076
+ "font-family",
2077
+ "font-size",
2078
+ "font-style",
2079
+ "font-weight",
2080
+ "href",
2081
+ "icon-height",
2082
+ "icon-padding",
2083
+ "icon-position",
2084
+ "icon-size",
2085
+ "line-height",
2086
+ "name",
2087
+ "padding",
2088
+ "padding-bottom",
2089
+ "padding-left",
2090
+ "padding-right",
2091
+ "padding-top",
2092
+ "rel",
2093
+ "sizes",
2094
+ "src",
2095
+ "srcset",
2096
+ "target",
2097
+ "text-decoration",
2098
+ "text-padding",
2099
+ "title",
2100
+ "vertical-align"
2101
+ ],
2102
+ "mj-hero": [
2103
+ "background-color",
2104
+ "background-height",
2105
+ "background-position",
2106
+ "background-url",
2107
+ "background-width",
2108
+ "border-radius",
2109
+ "container-background-color",
2110
+ "height",
2111
+ "inner-background-color",
2112
+ "inner-padding",
2113
+ "inner-padding-bottom",
2114
+ "inner-padding-left",
2115
+ "inner-padding-right",
2116
+ "inner-padding-top",
2117
+ "mode",
2118
+ "padding",
2119
+ "padding-bottom",
2120
+ "padding-left",
2121
+ "padding-right",
2122
+ "padding-top",
2123
+ "vertical-align"
2124
+ ],
2125
+ "mj-navbar": [
2126
+ "align",
2127
+ "base-url",
2128
+ "hamburger",
2129
+ "ico-align",
2130
+ "ico-close",
2131
+ "ico-color",
2132
+ "ico-font-family",
2133
+ "ico-font-size",
2134
+ "ico-line-height",
2135
+ "ico-open",
2136
+ "ico-padding",
2137
+ "ico-padding-bottom",
2138
+ "ico-padding-left",
2139
+ "ico-padding-right",
2140
+ "ico-padding-top",
2141
+ "ico-text-decoration",
2142
+ "ico-text-transform",
2143
+ "padding",
2144
+ "padding-bottom",
2145
+ "padding-left",
2146
+ "padding-right",
2147
+ "padding-top"
2148
+ ],
2149
+ "mj-navbar-link": [
2150
+ "color",
2151
+ "font-family",
2152
+ "font-size",
2153
+ "font-style",
2154
+ "font-weight",
2155
+ "href",
2156
+ "letter-spacing",
2157
+ "line-height",
2158
+ "name",
2159
+ "padding",
2160
+ "padding-bottom",
2161
+ "padding-left",
2162
+ "padding-right",
2163
+ "padding-top",
2164
+ "rel",
2165
+ "target",
2166
+ "text-decoration",
2167
+ "text-transform"
2168
+ ]
2169
+ };
2170
+ var ALWAYS_ALLOWED = /* @__PURE__ */ new Set(["mj-class", "css-class", "id", "style"]);
2171
+ var START_TAG = /<[a-z][^\s/>]*(?:[^>"']|"[^"]*"|'[^']*')*>/gi;
2172
+ var ATTRIBUTE = /\s+([^\s"'>/=]+)(?:\s*=\s*(?:"[^"]*"|'[^']*'|[^\s"'>]+))?/g;
2173
+ function withoutEditorProps(mjml) {
2174
+ return mjml.replace(
2175
+ START_TAG,
2176
+ (tag) => tag.replace(
2177
+ ATTRIBUTE,
2178
+ (attribute, name) => name.toLowerCase().startsWith("data-gjs-") ? "" : attribute
2179
+ )
2180
+ );
2181
+ }
2182
+ function withClosedTags(mjml) {
2183
+ return mjml.replace(START_TAG, (tag) => {
2184
+ const selfClosed = /^<(mj-[\w-]+)([\s\S]*?)\s*\/>$/i.exec(tag);
2185
+ return selfClosed ? `<${selfClosed[1]}${selfClosed[2]}></${selfClosed[1]}>` : tag;
2186
+ });
2187
+ }
2188
+ var TABLE = /<!--[\s\S]*?(?:-->|$)|(<mj-table\b(?:[^>"']|"[^"]*"|'[^']*')*>)((?:<!--[\s\S]*?-->|(?!<!--)[\s\S])*?)(<\/mj-table\s*>)/gi;
2189
+ function withTableRowsKept(mjml) {
2190
+ return mjml.replace(
2191
+ TABLE,
2192
+ (match, open, rows, close) => open ? `${open}<template>${rows}</template>${close}` : match
2193
+ );
2194
+ }
2195
+ function sourceForEditor(mjml) {
2196
+ const cleaned = withoutEditorProps(mjml);
2197
+ return { mjml: withTableRowsKept(withClosedTags(cleaned)), repaired: cleaned !== mjml };
2198
+ }
2199
+ function illegalAttributes(tag, attributes) {
2200
+ const allowed = ALLOWED_ATTRIBUTES[tag];
2201
+ if (!allowed) return [];
2202
+ return Object.keys(attributes).filter((a) => !ALWAYS_ALLOWED.has(a) && !allowed.includes(a));
2203
+ }
2204
+ var NAMED_COLORS = /* @__PURE__ */ new Set([
2205
+ "aliceblue",
2206
+ "antiquewhite",
2207
+ "aqua",
2208
+ "aquamarine",
2209
+ "azure",
2210
+ "beige",
2211
+ "bisque",
2212
+ "black",
2213
+ "blanchedalmond",
2214
+ "blue",
2215
+ "blueviolet",
2216
+ "brown",
2217
+ "burlywood",
2218
+ "cadetblue",
2219
+ "chartreuse",
2220
+ "chocolate",
2221
+ "coral",
2222
+ "cornflowerblue",
2223
+ "cornsilk",
2224
+ "crimson",
2225
+ "cyan",
2226
+ "darkblue",
2227
+ "darkcyan",
2228
+ "darkgoldenrod",
2229
+ "darkgray",
2230
+ "darkgreen",
2231
+ "darkgrey",
2232
+ "darkkhaki",
2233
+ "darkmagenta",
2234
+ "darkolivegreen",
2235
+ "darkorange",
2236
+ "darkorchid",
2237
+ "darkred",
2238
+ "darksalmon",
2239
+ "darkseagreen",
2240
+ "darkslateblue",
2241
+ "darkslategray",
2242
+ "darkslategrey",
2243
+ "darkturquoise",
2244
+ "darkviolet",
2245
+ "deeppink",
2246
+ "deepskyblue",
2247
+ "dimgray",
2248
+ "dimgrey",
2249
+ "dodgerblue",
2250
+ "firebrick",
2251
+ "floralwhite",
2252
+ "forestgreen",
2253
+ "fuchsia",
2254
+ "gainsboro",
2255
+ "ghostwhite",
2256
+ "gold",
2257
+ "goldenrod",
2258
+ "gray",
2259
+ "green",
2260
+ "greenyellow",
2261
+ "grey",
2262
+ "honeydew",
2263
+ "hotpink",
2264
+ "indianred",
2265
+ "indigo",
2266
+ "inherit",
2267
+ "ivory",
2268
+ "khaki",
2269
+ "lavender",
2270
+ "lavenderblush",
2271
+ "lawngreen",
2272
+ "lemonchiffon",
2273
+ "lightblue",
2274
+ "lightcoral",
2275
+ "lightcyan",
2276
+ "lightgoldenrodyellow",
2277
+ "lightgray",
2278
+ "lightgreen",
2279
+ "lightgrey",
2280
+ "lightpink",
2281
+ "lightsalmon",
2282
+ "lightseagreen",
2283
+ "lightskyblue",
2284
+ "lightslategray",
2285
+ "lightslategrey",
2286
+ "lightsteelblue",
2287
+ "lightyellow",
2288
+ "lime",
2289
+ "limegreen",
2290
+ "linen",
2291
+ "magenta",
2292
+ "maroon",
2293
+ "mediumaquamarine",
2294
+ "mediumblue",
2295
+ "mediumorchid",
2296
+ "mediumpurple",
2297
+ "mediumseagreen",
2298
+ "mediumslateblue",
2299
+ "mediumspringgreen",
2300
+ "mediumturquoise",
2301
+ "mediumvioletred",
2302
+ "midnightblue",
2303
+ "mintcream",
2304
+ "mistyrose",
2305
+ "moccasin",
2306
+ "navajowhite",
2307
+ "navy",
2308
+ "oldlace",
2309
+ "olive",
2310
+ "olivedrab",
2311
+ "orange",
2312
+ "orangered",
2313
+ "orchid",
2314
+ "palegoldenrod",
2315
+ "palegreen",
2316
+ "paleturquoise",
2317
+ "palevioletred",
2318
+ "papayawhip",
2319
+ "peachpuff",
2320
+ "peru",
2321
+ "pink",
2322
+ "plum",
2323
+ "powderblue",
2324
+ "purple",
2325
+ "rebeccapurple",
2326
+ "red",
2327
+ "rosybrown",
2328
+ "royalblue",
2329
+ "saddlebrown",
2330
+ "salmon",
2331
+ "sandybrown",
2332
+ "seagreen",
2333
+ "seashell",
2334
+ "sienna",
2335
+ "silver",
2336
+ "skyblue",
2337
+ "slateblue",
2338
+ "slategray",
2339
+ "slategrey",
2340
+ "snow",
2341
+ "springgreen",
2342
+ "steelblue",
2343
+ "tan",
2344
+ "teal",
2345
+ "thistle",
2346
+ "tomato",
2347
+ "transparent",
2348
+ "turquoise",
2349
+ "violet",
2350
+ "wheat",
2351
+ "white",
2352
+ "whitesmoke",
2353
+ "yellow",
2354
+ "yellowgreen"
2355
+ ]);
2356
+
2357
+ // src/components/editor/ColorField.tsx
2358
+ var import_jsx_runtime13 = require("react/jsx-runtime");
2359
+ var PRESETS = [
2360
+ "#000000",
2361
+ "#374151",
2362
+ "#6b7280",
2363
+ "#d1d5db",
2364
+ "#f3f4f6",
2365
+ "#ffffff",
2366
+ "#dc2626",
2367
+ "#ea580c",
2368
+ "#ca8a04",
2369
+ "#16a34a",
2370
+ "#0284c7",
2371
+ "#4f46e5",
2372
+ "#9333ea",
2373
+ "#db2777"
2374
+ ];
2375
+ var eyeDropper = () => globalThis.EyeDropper;
2376
+ function ColorField({
2377
+ value,
2378
+ placeholder,
2379
+ documentColors: documentColors2,
2380
+ labels,
2381
+ onChange
2382
+ }) {
2383
+ const field = useFieldLabel();
2384
+ const [text, setText] = (0, import_react6.useState)(value);
2385
+ const [open, setOpen] = (0, import_react6.useState)(false);
2386
+ const [used, setUsed] = (0, import_react6.useState)([]);
2387
+ const pressed = (0, import_react6.useRef)(false);
2388
+ const dragged = (0, import_react6.useRef)(null);
2389
+ const fieldRef = (0, import_react6.useRef)(null);
2390
+ const anchor = (0, import_react6.useRef)({
2391
+ getBoundingClientRect: () => {
2392
+ const field2 = fieldRef.current?.getBoundingClientRect() ?? new DOMRect();
2393
+ const panel = fieldRef.current?.closest(".editor-dark")?.getBoundingClientRect();
2394
+ return new DOMRect(panel?.right ?? field2.right, field2.top, 0, field2.height);
2395
+ }
2396
+ });
2397
+ (0, import_react6.useEffect)(() => setText(value), [value]);
2398
+ const latestOnChange = (0, import_react6.useRef)(onChange);
2399
+ latestOnChange.current = onChange;
2400
+ const release = (0, import_react6.useCallback)(() => {
2401
+ pressed.current = false;
2402
+ if (dragged.current !== null) latestOnChange.current(dragged.current, false);
2403
+ dragged.current = null;
2404
+ }, []);
2405
+ (0, import_react6.useEffect)(() => {
2406
+ document.addEventListener("lostpointercapture", release);
2407
+ return () => {
2408
+ document.removeEventListener("lostpointercapture", release);
2409
+ release();
2410
+ };
2411
+ }, [release]);
2412
+ const hex = toHex(value) ?? toHex(placeholder ?? "") ?? "#000000";
2413
+ const translucent = hex.length === 9;
2414
+ const shown = value || placeholder || "";
2415
+ const commit = (next) => {
2416
+ dragged.current = null;
2417
+ const color = mjmlColor(next);
2418
+ if (color === null) return setText(value);
2419
+ setText(color);
2420
+ onChange(color, false);
2421
+ };
2422
+ const drag = (picked) => {
2423
+ const next = mjmlColor(picked) ?? picked;
2424
+ setText(next);
2425
+ if (!pressed.current) return commit(next);
2426
+ dragged.current = next;
2427
+ onChange(next, true);
2428
+ };
2429
+ const pickFromScreen = async () => {
2430
+ const Ctor = eyeDropper();
2431
+ if (!Ctor) return;
2432
+ try {
2433
+ const { sRGBHex } = await new Ctor().open();
2434
+ commit(sRGBHex);
2435
+ } catch {
2436
+ }
2437
+ };
2438
+ return /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)(
2439
+ Popover,
2440
+ {
2441
+ open,
2442
+ onOpenChange: (next) => {
2443
+ if (next) setUsed(documentColors2());
2444
+ else if (text.trim() !== value) commit(text.trim());
2445
+ setOpen(next);
2446
+ },
2447
+ children: [
2448
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(PopoverAnchor, { virtualRef: anchor }),
2449
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)(
2450
+ "div",
2451
+ {
2452
+ ref: fieldRef,
2453
+ className: "flex h-8 w-full items-center gap-1.5 rounded-md border border-panel-border bg-panel-elevated pr-1.5 pl-2 focus-within:border-brand",
2454
+ children: [
2455
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
2456
+ "input",
2457
+ {
2458
+ value: text,
2459
+ placeholder,
2460
+ spellCheck: false,
2461
+ ...field,
2462
+ onChange: (e) => setText(e.target.value),
2463
+ onBlur: () => text !== value && commit(text.trim()),
2464
+ onKeyDown: (e) => {
2465
+ if (e.key === "Enter") commit(text.trim());
2466
+ if (e.key === "Escape") setText(value);
2467
+ },
2468
+ className: "min-w-0 flex-1 bg-transparent font-mono text-xs text-panel-fg outline-none placeholder:text-panel-muted-fg"
2469
+ }
2470
+ ),
2471
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(PopoverTrigger, { asChild: true, children: /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)(
2472
+ "button",
2473
+ {
2474
+ type: "button",
2475
+ title: labels.colorPickerOpen,
2476
+ "aria-label": labels.colorPickerOpen,
2477
+ className: "relative size-5 shrink-0 overflow-hidden rounded border border-white/20 outline-none focus-visible:ring-2 focus-visible:ring-brand",
2478
+ children: [
2479
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(Checkerboard, {}),
2480
+ mjmlColor(shown) && /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("span", { className: "absolute inset-0", style: { background: shown } })
2481
+ ]
2482
+ }
2483
+ ) })
2484
+ ]
2485
+ }
2486
+ ),
2487
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)(
2488
+ PopoverContent,
2489
+ {
2490
+ side: "right",
2491
+ align: "start",
2492
+ sideOffset: 8,
2493
+ className: "editor-dark flex w-52 flex-col gap-2.5 border border-panel-border bg-panel p-2.5 text-panel-fg",
2494
+ children: [
2495
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
2496
+ "div",
1157
2497
  {
1158
2498
  onPointerDownCapture: (e) => {
1159
2499
  pressed.current = true;
1160
2500
  e.target.setPointerCapture(e.pointerId);
1161
2501
  },
1162
- children: translucent ? /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_react_colorful.HexAlphaColorPicker, { color: hex, onChange: drag, className: "color-field-picker" }) : /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_react_colorful.HexColorPicker, { color: hex, onChange: drag, className: "color-field-picker" })
2502
+ children: translucent ? /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(import_react_colorful.HexAlphaColorPicker, { color: hex, onChange: drag, className: "color-field-picker" }) : /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(import_react_colorful.HexColorPicker, { color: hex, onChange: drag, className: "color-field-picker" })
1163
2503
  }
1164
2504
  ),
1165
- /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { className: "flex items-center gap-1.5", children: [
1166
- /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
2505
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("div", { className: "flex items-center gap-1.5", children: [
2506
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
1167
2507
  "span",
1168
2508
  {
1169
2509
  className: "size-6 shrink-0 rounded border border-white/20",
1170
2510
  style: { background: hex }
1171
2511
  }
1172
2512
  ),
1173
- /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
2513
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
1174
2514
  "input",
1175
2515
  {
1176
2516
  value: text,
1177
2517
  spellCheck: false,
2518
+ ...field,
1178
2519
  onChange: (e) => setText(e.target.value),
1179
2520
  onKeyDown: (e) => e.key === "Enter" && commit(text.trim()),
1180
2521
  onBlur: () => text !== value && commit(text.trim()),
1181
2522
  className: "h-6 min-w-0 flex-1 rounded border border-panel-border bg-panel-elevated px-1.5 font-mono text-[11px] outline-none focus:border-brand"
1182
2523
  }
1183
2524
  ),
1184
- eyeDropper() && /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
2525
+ eyeDropper() && /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
1185
2526
  "button",
1186
2527
  {
1187
2528
  type: "button",
@@ -1189,11 +2530,11 @@ function ColorField({
1189
2530
  title: labels.colorPickerEyedropper,
1190
2531
  "aria-label": labels.colorPickerEyedropper,
1191
2532
  className: "flex size-6 shrink-0 items-center justify-center rounded border border-panel-border bg-panel-elevated text-panel-muted-fg hover:text-panel-fg",
1192
- children: /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_lucide_react5.Pipette, { className: "size-3" })
2533
+ children: /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(import_lucide_react6.Pipette, { className: "size-3" })
1193
2534
  }
1194
2535
  )
1195
2536
  ] }),
1196
- used.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
2537
+ used.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
1197
2538
  Swatches,
1198
2539
  {
1199
2540
  title: labels.colorPickerDocument,
@@ -1202,7 +2543,7 @@ function ColorField({
1202
2543
  onPick: commit
1203
2544
  }
1204
2545
  ),
1205
- /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
2546
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
1206
2547
  Swatches,
1207
2548
  {
1208
2549
  title: labels.colorPickerPresets,
@@ -1226,10 +2567,10 @@ function Swatches({
1226
2567
  onPick,
1227
2568
  transparentLabel
1228
2569
  }) {
1229
- return /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { children: [
1230
- /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("p", { className: "mb-1 text-[10px] font-medium tracking-wide text-panel-muted-fg uppercase", children: title }),
1231
- /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { className: "grid grid-cols-8 gap-1", children: [
1232
- colors.map((color) => /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
2570
+ return /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("div", { children: [
2571
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("p", { className: "mb-1 text-[10px] font-medium tracking-wide text-panel-muted-fg uppercase", children: title }),
2572
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("div", { className: "grid grid-cols-8 gap-1", children: [
2573
+ colors.map((color) => /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
1233
2574
  "button",
1234
2575
  {
1235
2576
  type: "button",
@@ -1243,134 +2584,1630 @@ function Swatches({
1243
2584
  },
1244
2585
  color
1245
2586
  )),
1246
- transparentLabel && /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
2587
+ transparentLabel && /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
2588
+ "button",
2589
+ {
2590
+ type: "button",
2591
+ title: transparentLabel,
2592
+ onClick: () => onPick("transparent"),
2593
+ className: "relative aspect-square overflow-hidden rounded border border-white/15 outline-none hover:scale-110 focus-visible:ring-2 focus-visible:ring-brand",
2594
+ children: /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(Checkerboard, {})
2595
+ }
2596
+ )
2597
+ ] })
2598
+ ] });
2599
+ }
2600
+ function Checkerboard() {
2601
+ return /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
2602
+ "span",
2603
+ {
2604
+ className: "absolute inset-0",
2605
+ style: {
2606
+ backgroundImage: "repeating-conic-gradient(#9ca3af 0% 25%, #e5e7eb 0% 50%)",
2607
+ backgroundSize: "8px 8px"
2608
+ }
2609
+ }
2610
+ );
2611
+ }
2612
+ function mjmlColor(value) {
2613
+ const v = value.trim();
2614
+ if (v === "") return "";
2615
+ if (NAMED_COLORS.has(v.toLowerCase())) return v.toLowerCase();
2616
+ const hex = toHex(v);
2617
+ if (!hex) return null;
2618
+ if (hex.length === 7) return hex;
2619
+ const [r, g, b, a] = [1, 3, 5, 7].map((i) => parseInt(hex.slice(i, i + 2), 16));
2620
+ return `rgba(${r},${g},${b},${Number((a / 255).toFixed(2))})`;
2621
+ }
2622
+ function toHex(value, { opaque = false } = {}) {
2623
+ const v = value.trim().toLowerCase();
2624
+ const short = /^#([0-9a-f])([0-9a-f])([0-9a-f])$/.exec(v);
2625
+ if (short) return `#${short[1]}${short[1]}${short[2]}${short[2]}${short[3]}${short[3]}`;
2626
+ if (/^#[0-9a-f]{6}$/.test(v)) return v;
2627
+ if (/^#[0-9a-f]{8}$/.test(v)) return opaque || v.endsWith("ff") ? v.slice(0, 7) : v;
2628
+ const rgb = /^rgba?\(\s*(\d+)[\s,]+(\d+)[\s,]+(\d+)(?:[\s,/]+([\d.]+%?))?/.exec(v);
2629
+ if (rgb) {
2630
+ const channels = [rgb[1], rgb[2], rgb[3]].map((n) => Math.min(255, Number(n)));
2631
+ const alphaText = rgb[4];
2632
+ const alpha = alphaText?.endsWith("%") ? parseFloat(alphaText) / 100 : Number(alphaText ?? 1);
2633
+ if (!opaque && alpha < 1) channels.push(Math.round(Math.max(0, alpha) * 255));
2634
+ return `#${channels.map((n) => n.toString(16).padStart(2, "0")).join("")}`;
2635
+ }
2636
+ return null;
2637
+ }
2638
+
2639
+ // src/components/editor/ImageField.tsx
2640
+ var import_lucide_react7 = require("lucide-react");
2641
+ var import_jsx_runtime14 = require("react/jsx-runtime");
2642
+ function ImageField({
2643
+ value,
2644
+ labels,
2645
+ onChoose,
2646
+ onClear
2647
+ }) {
2648
+ const url = imageUrl(value);
2649
+ return /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)("div", { className: "relative", children: [
2650
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(
2651
+ "button",
2652
+ {
2653
+ type: "button",
2654
+ onClick: onChoose,
2655
+ title: labels.chooseImage,
2656
+ className: "group relative block w-full overflow-hidden rounded-md border border-panel-border bg-panel-elevated outline-none focus-visible:ring-2 focus-visible:ring-brand",
2657
+ children: [
2658
+ url ? /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("img", { src: url, alt: "", className: "h-24 w-full bg-black/25 object-contain" }) : /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)("span", { className: "flex h-16 flex-col items-center justify-center gap-1 text-xs text-panel-muted-fg", children: [
2659
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_lucide_react7.ImageIcon, { className: "size-5" }),
2660
+ labels.chooseImage
2661
+ ] }),
2662
+ url && /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("span", { className: "absolute inset-0 flex items-center justify-center bg-black/50 text-xs font-medium text-white opacity-0 transition group-hover:opacity-100", children: labels.chooseImage })
2663
+ ]
2664
+ }
2665
+ ),
2666
+ value && /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
2667
+ "button",
2668
+ {
2669
+ type: "button",
2670
+ onClick: onClear,
2671
+ title: labels.imageFieldRemove,
2672
+ "aria-label": labels.imageFieldRemove,
2673
+ className: "absolute top-1 right-1 flex size-5 items-center justify-center rounded bg-black/60 text-white hover:bg-black/80",
2674
+ children: /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_lucide_react7.X, { className: "size-3" })
2675
+ }
2676
+ )
2677
+ ] });
2678
+ }
2679
+ function imageUrl(value) {
2680
+ const v = value.trim();
2681
+ if (!v || v === "none") return null;
2682
+ const css = /^url\(\s*(['"]?)(.*?)\1\s*\)$/i.exec(v);
2683
+ if (css) return css[2] || null;
2684
+ return /^[a-z-]+\(/i.test(v) ? null : v;
2685
+ }
2686
+
2687
+ // src/components/editor/inspector/schema.ts
2688
+ var PX = ["px"];
2689
+ var PX_PCT = ["px", "%"];
2690
+ var LINE_HEIGHT = ["", "px", "%"];
2691
+ var LETTER_SPACING = ["px", "em"];
2692
+ var ALIGN = [
2693
+ { value: "left", label: "alignLeft" },
2694
+ { value: "center", label: "alignCenter" },
2695
+ { value: "right", label: "alignRight" }
2696
+ ];
2697
+ var VERTICAL_ALIGN = [
2698
+ { value: "top", label: "alignTop" },
2699
+ { value: "middle", label: "alignMiddle" },
2700
+ { value: "bottom", label: "alignBottom" }
2701
+ ];
2702
+ var FONTS = [
2703
+ { value: "Ubuntu, Helvetica, Arial, sans-serif", label: "Ubuntu" },
2704
+ { value: "Arial, Helvetica, sans-serif", label: "Arial" },
2705
+ { value: "Helvetica, Arial, sans-serif", label: "Helvetica" },
2706
+ { value: "Verdana, Geneva, sans-serif", label: "Verdana" },
2707
+ { value: "Tahoma, Geneva, sans-serif", label: "Tahoma" },
2708
+ { value: "'Trebuchet MS', Helvetica, sans-serif", label: "Trebuchet MS" },
2709
+ { value: "Georgia, 'Times New Roman', serif", label: "Georgia" },
2710
+ { value: "'Times New Roman', Times, serif", label: "Times New Roman" },
2711
+ { value: "'Courier New', Courier, monospace", label: "Courier New" }
2712
+ ];
2713
+ var WEIGHTS = [
2714
+ { value: "normal", label: "weightRegular" },
2715
+ { value: "500", label: "weightMedium" },
2716
+ { value: "600", label: "weightSemibold" },
2717
+ { value: "bold", label: "weightBold" }
2718
+ ];
2719
+ var TEXT_TRANSFORM = [
2720
+ { value: "none", label: "caseNone" },
2721
+ { value: "uppercase", label: "caseUpper" },
2722
+ { value: "lowercase", label: "caseLower" },
2723
+ { value: "capitalize", label: "caseCapitalize" }
2724
+ ];
2725
+ var TARGET = [
2726
+ { value: "_blank", label: "targetNewTab" },
2727
+ { value: "_self", label: "targetSameTab" }
2728
+ ];
2729
+ var BACKGROUND_POSITION = [
2730
+ { value: "top left", label: "positionTopLeft" },
2731
+ { value: "top center", label: "positionTopCenter" },
2732
+ { value: "top right", label: "positionTopRight" },
2733
+ { value: "center left", label: "positionCenterLeft" },
2734
+ { value: "center center", label: "positionCenter" },
2735
+ { value: "center right", label: "positionCenterRight" },
2736
+ { value: "bottom left", label: "positionBottomLeft" },
2737
+ { value: "bottom center", label: "positionBottomCenter" },
2738
+ { value: "bottom right", label: "positionBottomRight" }
2739
+ ];
2740
+ var NETWORKS = [
2741
+ { value: "facebook", label: "Facebook" },
2742
+ { value: "instagram", label: "Instagram" },
2743
+ { value: "x", label: "X" },
2744
+ { value: "linkedin", label: "LinkedIn" },
2745
+ { value: "youtube", label: "YouTube" },
2746
+ { value: "pinterest", label: "Pinterest" },
2747
+ { value: "github", label: "GitHub" },
2748
+ { value: "web", label: "networkWebsite" },
2749
+ { value: "snapchat", label: "Snapchat" },
2750
+ { value: "vimeo", label: "Vimeo" },
2751
+ { value: "medium", label: "Medium" },
2752
+ { value: "dribbble", label: "Dribbble" },
2753
+ { value: "soundcloud", label: "SoundCloud" },
2754
+ { value: "tumblr", label: "Tumblr" },
2755
+ { value: "xing", label: "Xing" },
2756
+ { value: "custom", label: "networkCustom" }
2757
+ ];
2758
+ var hasBackgroundImage = (a) => !!a["background-url"];
2759
+ var SECTION_FIELDS = [
2760
+ {
2761
+ attr: "full-width",
2762
+ label: "fullWidth",
2763
+ group: "layout",
2764
+ control: { type: "toggle", on: "full-width" }
2765
+ },
2766
+ {
2767
+ attr: "padding",
2768
+ label: "padding",
2769
+ group: "spacing",
2770
+ control: { type: "box", units: PX_PCT },
2771
+ default: "20px 0"
2772
+ },
2773
+ {
2774
+ attr: "background-color",
2775
+ label: "backgroundColor",
2776
+ group: "style",
2777
+ control: { type: "color" }
2778
+ },
2779
+ { attr: "background-url", label: "backgroundImage", group: "style", control: { type: "image" } },
2780
+ {
2781
+ attr: "background-size",
2782
+ label: "backgroundSize",
2783
+ group: "style",
2784
+ control: {
2785
+ type: "segmented",
2786
+ options: [
2787
+ { value: "cover", label: "sizeCover" },
2788
+ { value: "contain", label: "sizeContain" },
2789
+ { value: "auto", label: "sizeAuto" }
2790
+ ]
2791
+ },
2792
+ default: "auto",
2793
+ when: hasBackgroundImage
2794
+ },
2795
+ {
2796
+ attr: "background-repeat",
2797
+ label: "backgroundRepeat",
2798
+ group: "style",
2799
+ control: {
2800
+ type: "segmented",
2801
+ options: [
2802
+ { value: "repeat", label: "repeatTile" },
2803
+ { value: "no-repeat", label: "repeatOnce" }
2804
+ ]
2805
+ },
2806
+ default: "repeat",
2807
+ when: hasBackgroundImage
2808
+ },
2809
+ {
2810
+ attr: "background-position",
2811
+ label: "backgroundPosition",
2812
+ group: "style",
2813
+ control: { type: "select", options: BACKGROUND_POSITION },
2814
+ default: "top center",
2815
+ when: hasBackgroundImage
2816
+ },
2817
+ {
2818
+ attr: "border-radius",
2819
+ label: "cornerRadius",
2820
+ group: "style",
2821
+ control: { type: "length", units: PX_PCT }
2822
+ }
2823
+ ];
2824
+ var SCHEMA = {
2825
+ "mj-body": [
2826
+ { attr: "width", label: "contentWidth", group: "layout", control: { type: "length", units: PX }, default: "600px" },
2827
+ { attr: "background-color", label: "pageBackground", group: "style", control: { type: "color" } }
2828
+ ],
2829
+ "mj-wrapper": [
2830
+ ...SECTION_FIELDS,
2831
+ { attr: "gap", label: "gap", group: "advanced", control: { type: "length", units: PX } },
2832
+ { attr: "border", label: "border", group: "advanced", control: { type: "text", placeholder: "1px solid #cccccc" } }
2833
+ ],
2834
+ "mj-section": [
2835
+ ...SECTION_FIELDS,
2836
+ { attr: "text-align", label: "alignColumns", group: "advanced", control: { type: "segmented", options: ALIGN }, default: "center" },
2837
+ { attr: "direction", label: "reverseColumns", group: "advanced", control: { type: "toggle", on: "rtl" } },
2838
+ { attr: "border", label: "border", group: "advanced", control: { type: "text", placeholder: "1px solid #cccccc" } }
2839
+ ],
2840
+ "mj-column": [
2841
+ { attr: "vertical-align", label: "verticalAlign", group: "layout", control: { type: "segmented", options: VERTICAL_ALIGN }, default: "top" },
2842
+ { attr: "padding", label: "padding", group: "spacing", control: { type: "box", units: PX_PCT } },
2843
+ { attr: "background-color", label: "backgroundColor", group: "style", control: { type: "color" } },
2844
+ { attr: "border-radius", label: "cornerRadius", group: "style", control: { type: "length", units: PX_PCT } },
2845
+ { attr: "width", label: "width", group: "advanced", control: { type: "length", units: PX_PCT } },
2846
+ { attr: "inner-background-color", label: "innerBackground", group: "advanced", control: { type: "color" } },
2847
+ { attr: "inner-border-radius", label: "innerCornerRadius", group: "advanced", control: { type: "length", units: PX_PCT } },
2848
+ { attr: "border", label: "border", group: "advanced", control: { type: "text", placeholder: "1px solid #cccccc" } },
2849
+ { attr: "inner-border", label: "innerBorder", group: "advanced", control: { type: "text", placeholder: "1px solid #cccccc" } }
2850
+ ],
2851
+ "mj-text": [
2852
+ {
2853
+ attr: "align",
2854
+ label: "align",
2855
+ group: "layout",
2856
+ control: { type: "segmented", options: [...ALIGN, { value: "justify", label: "alignJustify" }] },
2857
+ default: "left"
2858
+ },
2859
+ { attr: "padding", label: "padding", group: "spacing", control: { type: "box", units: PX_PCT }, default: "10px 25px" },
2860
+ { attr: "font-family", label: "font", group: "style", control: { type: "select", options: FONTS }, default: "Ubuntu, Helvetica, Arial, sans-serif" },
2861
+ { attr: "font-size", label: "fontSize", group: "style", control: { type: "length", units: PX }, default: "13px" },
2862
+ { attr: "font-weight", label: "fontWeight", group: "style", control: { type: "select", options: WEIGHTS } },
2863
+ { attr: "color", label: "textColor", group: "style", control: { type: "color" }, default: "#000000" },
2864
+ { attr: "line-height", label: "lineHeight", group: "style", control: { type: "length", units: LINE_HEIGHT }, default: "1" },
2865
+ { attr: "container-background-color", label: "blockBackground", group: "style", control: { type: "color" } },
2866
+ { attr: "letter-spacing", label: "letterSpacing", group: "advanced", control: { type: "length", units: LETTER_SPACING, negative: true } },
2867
+ { attr: "text-transform", label: "textTransform", group: "advanced", control: { type: "select", options: TEXT_TRANSFORM } }
2868
+ ],
2869
+ "mj-button": [
2870
+ { attr: "href", label: "link", group: "content", control: { type: "link" } },
2871
+ { attr: "align", label: "align", group: "layout", control: { type: "segmented", options: ALIGN }, default: "center" },
2872
+ { attr: "inner-padding", label: "buttonPadding", group: "spacing", control: { type: "box", units: PX_PCT }, default: "10px 25px" },
2873
+ { attr: "padding", label: "spaceAround", group: "spacing", control: { type: "box", units: PX_PCT }, default: "10px 25px" },
2874
+ { attr: "background-color", label: "buttonColor", group: "style", control: { type: "color" }, default: "#414141" },
2875
+ { attr: "color", label: "textColor", group: "style", control: { type: "color" }, default: "#ffffff" },
2876
+ { attr: "font-size", label: "fontSize", group: "style", control: { type: "length", units: PX }, default: "13px" },
2877
+ { attr: "border-radius", label: "cornerRadius", group: "style", control: { type: "length", units: PX_PCT }, default: "3px" },
2878
+ { attr: "width", label: "width", group: "advanced", control: { type: "length", units: PX_PCT } },
2879
+ { attr: "font-family", label: "font", group: "advanced", control: { type: "select", options: FONTS }, default: "Ubuntu, Helvetica, Arial, sans-serif" },
2880
+ { attr: "font-weight", label: "fontWeight", group: "advanced", control: { type: "select", options: WEIGHTS }, default: "normal" },
2881
+ { attr: "target", label: "opensIn", group: "advanced", control: { type: "select", options: TARGET }, default: "_blank" },
2882
+ { attr: "title", label: "tooltip", group: "advanced", control: { type: "text" } },
2883
+ { attr: "rel", label: "rel", group: "advanced", control: { type: "text", placeholder: "noopener" } },
2884
+ { attr: "container-background-color", label: "blockBackground", group: "advanced", control: { type: "color" } },
2885
+ { attr: "border", label: "border", group: "advanced", control: { type: "text", placeholder: "1px solid #cccccc" }, default: "none" },
2886
+ { attr: "letter-spacing", label: "letterSpacing", group: "advanced", control: { type: "length", units: LETTER_SPACING, negative: true } },
2887
+ { attr: "text-transform", label: "textTransform", group: "advanced", control: { type: "select", options: TEXT_TRANSFORM }, default: "none" },
2888
+ { attr: "line-height", label: "lineHeight", group: "advanced", control: { type: "length", units: LINE_HEIGHT }, default: "120%" }
2889
+ ],
2890
+ "mj-image": [
2891
+ { attr: "src", label: "image", group: "content", control: { type: "image" } },
2892
+ { attr: "alt", label: "altText", group: "content", control: { type: "text" } },
2893
+ { attr: "href", label: "link", group: "content", control: { type: "link" } },
2894
+ { attr: "width", label: "width", group: "layout", control: { type: "length", units: PX } },
2895
+ { attr: "align", label: "align", group: "layout", control: { type: "segmented", options: ALIGN }, default: "center" },
2896
+ { attr: "fluid-on-mobile", label: "fluidOnMobile", group: "layout", control: { type: "toggle", on: "true" } },
2897
+ { attr: "padding", label: "padding", group: "spacing", control: { type: "box", units: PX_PCT }, default: "10px 25px" },
2898
+ { attr: "border-radius", label: "cornerRadius", group: "style", control: { type: "length", units: PX_PCT } },
2899
+ { attr: "target", label: "opensIn", group: "advanced", control: { type: "select", options: TARGET }, default: "_blank" },
2900
+ { attr: "title", label: "tooltip", group: "advanced", control: { type: "text" } },
2901
+ { attr: "rel", label: "rel", group: "advanced", control: { type: "text", placeholder: "noopener" } },
2902
+ { attr: "border", label: "border", group: "advanced", control: { type: "text", placeholder: "1px solid #cccccc" }, default: "0" },
2903
+ { attr: "container-background-color", label: "blockBackground", group: "advanced", control: { type: "color" } }
2904
+ ],
2905
+ "mj-divider": [
2906
+ { attr: "width", label: "width", group: "layout", control: { type: "length", units: PX_PCT }, default: "100%" },
2907
+ { attr: "align", label: "align", group: "layout", control: { type: "segmented", options: ALIGN }, default: "center" },
2908
+ { attr: "padding", label: "padding", group: "spacing", control: { type: "box", units: PX_PCT }, default: "10px 25px" },
2909
+ { attr: "border-color", label: "lineColor", group: "style", control: { type: "color" }, default: "#000000" },
2910
+ { attr: "border-width", label: "thickness", group: "style", control: { type: "length", units: PX }, default: "4px" },
2911
+ {
2912
+ attr: "border-style",
2913
+ label: "lineStyle",
2914
+ group: "style",
2915
+ control: {
2916
+ type: "segmented",
2917
+ options: [
2918
+ { value: "solid", label: "lineSolid" },
2919
+ { value: "dashed", label: "lineDashed" },
2920
+ { value: "dotted", label: "lineDotted" }
2921
+ ]
2922
+ },
2923
+ default: "solid"
2924
+ },
2925
+ { attr: "container-background-color", label: "blockBackground", group: "advanced", control: { type: "color" } }
2926
+ ],
2927
+ "mj-spacer": [
2928
+ { attr: "height", label: "height", group: "layout", control: { type: "length", units: PX }, default: "20px" },
2929
+ { attr: "container-background-color", label: "blockBackground", group: "advanced", control: { type: "color" } }
2930
+ ],
2931
+ "mj-social": [
2932
+ {
2933
+ attr: "mode",
2934
+ label: "socialMode",
2935
+ group: "layout",
2936
+ control: {
2937
+ type: "segmented",
2938
+ options: [
2939
+ { value: "horizontal", label: "modeHorizontal" },
2940
+ { value: "vertical", label: "modeVertical" }
2941
+ ]
2942
+ },
2943
+ default: "horizontal"
2944
+ },
2945
+ { attr: "align", label: "align", group: "layout", control: { type: "segmented", options: ALIGN }, default: "center" },
2946
+ { attr: "padding", label: "padding", group: "spacing", control: { type: "box", units: PX_PCT }, default: "10px 25px" },
2947
+ { attr: "inner-padding", label: "iconSpacing", group: "spacing", control: { type: "box", units: PX_PCT } },
2948
+ { attr: "icon-size", label: "iconSize", group: "style", control: { type: "length", units: PX_PCT }, default: "20px" },
2949
+ // A single value: mj-social's border-radius is not a 4-side shorthand.
2950
+ { attr: "border-radius", label: "cornerRadius", group: "style", control: { type: "length", units: PX_PCT }, default: "3px" },
2951
+ { attr: "font-family", label: "font", group: "advanced", control: { type: "select", options: FONTS }, default: "Ubuntu, Helvetica, Arial, sans-serif" },
2952
+ { attr: "font-size", label: "fontSize", group: "advanced", control: { type: "length", units: PX }, default: "13px" },
2953
+ { attr: "color", label: "textColor", group: "advanced", control: { type: "color" }, default: "#333333" },
2954
+ { attr: "line-height", label: "lineHeight", group: "advanced", control: { type: "length", units: LINE_HEIGHT }, default: "22px" },
2955
+ { attr: "container-background-color", label: "blockBackground", group: "advanced", control: { type: "color" } }
2956
+ ],
2957
+ "mj-social-element": [
2958
+ { attr: "name", label: "network", group: "content", control: { type: "network", networks: NETWORKS } },
2959
+ { attr: "href", label: "link", group: "content", control: { type: "link" } },
2960
+ {
2961
+ attr: "src",
2962
+ label: "customIcon",
2963
+ group: "content",
2964
+ control: { type: "image" },
2965
+ when: (a) => (a.name ?? "").startsWith("custom") || !!a.src
2966
+ },
2967
+ { attr: "alt", label: "altText", group: "content", control: { type: "text" } },
2968
+ { attr: "background-color", label: "iconColor", group: "style", control: { type: "color" } },
2969
+ { attr: "icon-size", label: "iconSize", group: "advanced", control: { type: "length", units: PX_PCT } },
2970
+ { attr: "border-radius", label: "cornerRadius", group: "advanced", control: { type: "length", units: PX }, default: "3px" },
2971
+ { attr: "padding", label: "padding", group: "advanced", control: { type: "box", units: PX_PCT }, default: "4px" },
2972
+ { attr: "icon-position", label: "iconPosition", group: "advanced", control: { type: "segmented", options: [{ value: "left", label: "alignLeft" }, { value: "right", label: "alignRight" }] }, default: "left" },
2973
+ { attr: "title", label: "tooltip", group: "advanced", control: { type: "text" } },
2974
+ { attr: "target", label: "opensIn", group: "advanced", control: { type: "select", options: TARGET }, default: "_blank" },
2975
+ { attr: "rel", label: "rel", group: "advanced", control: { type: "text", placeholder: "noopener" } }
2976
+ ],
2977
+ "mj-hero": [
2978
+ { attr: "background-url", label: "backgroundImage", group: "content", control: { type: "image" } },
2979
+ {
2980
+ attr: "mode",
2981
+ label: "heroMode",
2982
+ group: "content",
2983
+ control: {
2984
+ type: "segmented",
2985
+ options: [
2986
+ { value: "fixed-height", label: "heroFixed" },
2987
+ { value: "fluid-height", label: "heroFluid" }
2988
+ ]
2989
+ },
2990
+ default: "fixed-height"
2991
+ },
2992
+ {
2993
+ attr: "height",
2994
+ label: "height",
2995
+ group: "layout",
2996
+ control: { type: "length", units: PX_PCT },
2997
+ default: "0px",
2998
+ when: (a) => (a.mode ?? "fixed-height") === "fixed-height"
2999
+ },
3000
+ {
3001
+ attr: "background-width",
3002
+ label: "backgroundWidth",
3003
+ group: "layout",
3004
+ control: { type: "length", units: PX_PCT },
3005
+ when: (a) => a.mode === "fluid-height"
3006
+ },
3007
+ {
3008
+ attr: "background-height",
3009
+ label: "backgroundHeight",
3010
+ group: "layout",
3011
+ control: { type: "length", units: PX_PCT },
3012
+ when: (a) => a.mode === "fluid-height"
3013
+ },
3014
+ { attr: "vertical-align", label: "verticalAlign", group: "layout", control: { type: "segmented", options: VERTICAL_ALIGN }, default: "top" },
3015
+ { attr: "background-position", label: "backgroundPosition", group: "layout", control: { type: "select", options: BACKGROUND_POSITION }, default: "center center" },
3016
+ { attr: "padding", label: "padding", group: "spacing", control: { type: "box", units: PX_PCT }, default: "0px" },
3017
+ { attr: "background-color", label: "fallbackColor", group: "style", control: { type: "color" }, default: "#ffffff" },
3018
+ { attr: "border-radius", label: "cornerRadius", group: "style", control: { type: "length", units: PX_PCT } },
3019
+ { attr: "inner-padding", label: "contentPadding", group: "advanced", control: { type: "box", units: PX_PCT } },
3020
+ { attr: "inner-background-color", label: "contentBackground", group: "advanced", control: { type: "color" } }
3021
+ ],
3022
+ "mj-navbar": [
3023
+ { attr: "align", label: "align", group: "layout", control: { type: "segmented", options: ALIGN }, default: "center" },
3024
+ { attr: "hamburger", label: "hamburger", group: "layout", control: { type: "toggle", on: "hamburger" } },
3025
+ { attr: "padding", label: "padding", group: "spacing", control: { type: "box", units: PX_PCT } },
3026
+ { attr: "ico-color", label: "menuIconColor", group: "advanced", control: { type: "color" }, default: "#000000", when: (a) => !!a.hamburger },
3027
+ { attr: "ico-font-size", label: "menuIconSize", group: "advanced", control: { type: "length", units: PX_PCT }, default: "30px", when: (a) => !!a.hamburger },
3028
+ { attr: "ico-align", label: "menuIconAlign", group: "advanced", control: { type: "segmented", options: ALIGN }, default: "center", when: (a) => !!a.hamburger },
3029
+ { attr: "base-url", label: "baseUrl", group: "advanced", control: { type: "link" } }
3030
+ ],
3031
+ "mj-navbar-link": [
3032
+ { attr: "href", label: "link", group: "content", control: { type: "link" } },
3033
+ { attr: "padding", label: "padding", group: "spacing", control: { type: "box", units: PX_PCT }, default: "15px 10px" },
3034
+ { attr: "color", label: "textColor", group: "style", control: { type: "color" }, default: "#000000" },
3035
+ { attr: "font-family", label: "font", group: "style", control: { type: "select", options: FONTS }, default: "Ubuntu, Helvetica, Arial, sans-serif" },
3036
+ { attr: "font-size", label: "fontSize", group: "style", control: { type: "length", units: PX }, default: "13px" },
3037
+ { attr: "font-weight", label: "fontWeight", group: "style", control: { type: "select", options: WEIGHTS }, default: "normal" },
3038
+ { attr: "text-transform", label: "textTransform", group: "style", control: { type: "select", options: TEXT_TRANSFORM }, default: "uppercase" },
3039
+ { attr: "target", label: "opensIn", group: "advanced", control: { type: "select", options: TARGET }, default: "_blank" },
3040
+ { attr: "rel", label: "rel", group: "advanced", control: { type: "text", placeholder: "noopener" } },
3041
+ { attr: "letter-spacing", label: "letterSpacing", group: "advanced", control: { type: "length", units: LETTER_SPACING, negative: true } },
3042
+ { attr: "line-height", label: "lineHeight", group: "advanced", control: { type: "length", units: LINE_HEIGHT }, default: "22px" }
3043
+ ]
3044
+ };
3045
+
3046
+ // src/components/editor/layers.ts
3047
+ var svg = (paths) => `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">${paths}</svg>`;
3048
+ var ICONS = {
3049
+ "mj-wrapper": svg(
3050
+ '<path d="M3 7V5a2 2 0 0 1 2-2h2"/><path d="M17 3h2a2 2 0 0 1 2 2v2"/><path d="M21 17v2a2 2 0 0 1-2 2h-2"/><path d="M7 21H5a2 2 0 0 1-2-2v-2"/><rect width="10" height="10" x="7" y="7" rx="1"/>'
3051
+ ),
3052
+ "mj-section": svg('<rect width="20" height="12" x="2" y="6" rx="2"/>'),
3053
+ "mj-hero": svg('<rect width="18" height="18" x="3" y="3" rx="2"/><path d="M3 9h18"/>'),
3054
+ "mj-group": svg(
3055
+ '<rect width="18" height="18" x="3" y="3" rx="2"/><path d="M9 3v18"/><path d="M15 3v18"/>'
3056
+ ),
3057
+ "mj-column": svg('<rect width="18" height="18" x="3" y="3" rx="2"/><path d="M12 3v18"/>'),
3058
+ "mj-text": svg('<path d="M4 7V4h16v3"/><path d="M9 20h6"/><path d="M12 4v16"/>'),
3059
+ "mj-image": svg(
3060
+ '<rect width="18" height="18" x="3" y="3" rx="2"/><circle cx="9" cy="9" r="2"/><path d="m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21"/>'
3061
+ ),
3062
+ "mj-button": svg('<rect width="20" height="10" x="2" y="7" rx="3"/><path d="M8 12h8"/>'),
3063
+ "mj-divider": svg('<path d="M5 12h14"/>'),
3064
+ "mj-spacer": svg('<path d="M12 3v18"/><path d="m8 17 4 4 4-4"/><path d="m8 7 4-4 4 4"/>'),
3065
+ "mj-social": svg(
3066
+ '<circle cx="18" cy="5" r="3"/><circle cx="6" cy="12" r="3"/><circle cx="18" cy="19" r="3"/><path d="m8.59 13.51 6.83 3.98"/><path d="m15.41 6.51-6.82 3.98"/>'
3067
+ ),
3068
+ "mj-social-element": svg(
3069
+ '<circle cx="12" cy="12" r="4"/><path d="M16 8v5a3 3 0 0 0 6 0v-1a10 10 0 1 0-4 8"/>'
3070
+ ),
3071
+ "mj-navbar": svg('<path d="M4 6h16"/><path d="M4 12h16"/><path d="M4 18h16"/>'),
3072
+ "mj-navbar-link": svg(
3073
+ '<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/>'
3074
+ ),
3075
+ "mj-raw": svg('<path d="m16 18 6-6-6-6"/><path d="m8 6-6 6 6 6"/>'),
3076
+ "mj-table": svg(
3077
+ '<rect width="18" height="18" x="3" y="3" rx="2"/><path d="M3 9h18"/><path d="M3 15h18"/><path d="M12 3v18"/>'
3078
+ )
3079
+ };
3080
+ function tagOf(c) {
3081
+ return String(c.get("tagName") || c.get("type") || "");
3082
+ }
3083
+ function textOf(c) {
3084
+ const template = document.createElement("template");
3085
+ template.innerHTML = c.getInnerHTML();
3086
+ template.content.querySelectorAll("br, p, div, li, tr, h1, h2, h3, h4, h5, h6").forEach((el) => el.after("\n"));
3087
+ const lines = (template.content.textContent ?? "").split("\n");
3088
+ const first = lines.map((l) => l.replace(/\s+/g, " ").trim()).find(Boolean) ?? "";
3089
+ return first.slice(0, 60);
3090
+ }
3091
+ function escapeHtml(s) {
3092
+ return s.replace(/[&<>"']/g, (ch) => `&#${ch.charCodeAt(0)};`);
3093
+ }
3094
+ function layerName(c, labels) {
3095
+ const tag = tagOf(c);
3096
+ const kind = labels.elementNames[tag] ?? tag.replace(/^mj-/, "");
3097
+ const attributes = c.getAttributes();
3098
+ switch (tag) {
3099
+ case "mj-text":
3100
+ case "mj-button":
3101
+ case "mj-navbar-link":
3102
+ return textOf(c) || kind;
3103
+ case "mj-image":
3104
+ return attributes.alt?.trim() || kind;
3105
+ case "mj-social-element": {
3106
+ const { network } = parseNetwork(String(attributes.name ?? ""));
3107
+ const option = NETWORKS.find((o) => o.value === network && network !== "custom");
3108
+ return textOf(c) || option && (labels.inspectorLabels[option.label] ?? option.label) || kind;
3109
+ }
3110
+ case "mj-section":
3111
+ case "mj-wrapper":
3112
+ case "mj-hero": {
3113
+ let words = "";
3114
+ c.onAll((d) => {
3115
+ if (!words && d !== c && ["mj-text", "mj-button"].includes(tagOf(d))) words = textOf(d);
3116
+ });
3117
+ return words || kind;
3118
+ }
3119
+ case "mj-column": {
3120
+ const columns = c.parent()?.components().filter((s) => tagOf(s) === "mj-column") ?? [];
3121
+ return columns.length > 1 ? `${kind} ${columns.indexOf(c) + 1}` : kind;
3122
+ }
3123
+ default:
3124
+ return kind;
3125
+ }
3126
+ }
3127
+ var CONTENT = /* @__PURE__ */ new Set(["mj-text", "mj-button", "mj-navbar-link", "mj-social-element", "mj-raw"]);
3128
+ function badgeLabel(labels) {
3129
+ return (c) => {
3130
+ const tag = tagOf(c);
3131
+ const icon = ICONS[tag] ? `<div class="gjs-badge__icon">${ICONS[tag]}</div>` : "";
3132
+ const kind = labels.elementNames[tag] ?? tag.replace(/^mj-/, "");
3133
+ return `${icon}<div class="gjs-badge__name">${escapeHtml(kind)}</div>`;
3134
+ };
3135
+ }
3136
+ function setupLayerNames(editor, labels) {
3137
+ const hideContent = (c) => c.onAll((d) => d !== c && d.set("layerable", false, { avoidStore: true }));
3138
+ for (const type of editor.Components.getTypes()) {
3139
+ if (!type.id.startsWith("mj-")) continue;
3140
+ editor.Components.addType(type.id, {
3141
+ model: {
3142
+ // Ours whatever the element says: GrapesJS writes it into the row's markup as it is.
3143
+ getIcon() {
3144
+ return ICONS[type.id] ?? "";
3145
+ },
3146
+ // GrapesJS writes the name into the row's markup unescaped, and it is document text.
3147
+ getName() {
3148
+ return escapeHtml(layerName(this, labels));
3149
+ },
3150
+ ...CONTENT.has(type.id) && {
3151
+ // Editing a text replaces its children, so they are hidden again as they arrive.
3152
+ init() {
3153
+ hideContent(this);
3154
+ this.listenTo(this.components(), "add reset", () => hideContent(this));
3155
+ }
3156
+ }
3157
+ },
3158
+ // Runs the plugin's own `init` as well.
3159
+ extendFn: ["init"]
3160
+ });
3161
+ }
3162
+ let pending = false;
3163
+ editor.on("update component:add component:remove", () => {
3164
+ if (pending) return;
3165
+ pending = true;
3166
+ requestAnimationFrame(() => {
3167
+ pending = false;
3168
+ editor.Layers.getRoot()?.onAll((c) => {
3169
+ const view = c.viewLayer;
3170
+ const el = view?.getInputName?.();
3171
+ const name = layerName(c, labels);
3172
+ if (!el || el.textContent === name) return;
3173
+ el.textContent = name;
3174
+ el.parentElement?.setAttribute("title", name);
3175
+ });
3176
+ });
3177
+ });
3178
+ }
3179
+
3180
+ // src/components/editor/inspector/StructureSection.tsx
3181
+ var import_lucide_react8 = require("lucide-react");
3182
+
3183
+ // src/components/editor/inspector/structure.ts
3184
+ function tagOf2(c) {
3185
+ return String(c.get("tagName") || c.get("type") || "");
3186
+ }
3187
+ var COLUMN_RATIOS = {
3188
+ 2: [
3189
+ [1, 1],
3190
+ [1, 2],
3191
+ [2, 1]
3192
+ ],
3193
+ 3: [
3194
+ [1, 1, 1],
3195
+ [1, 2, 1]
3196
+ ]
3197
+ };
3198
+ function columnsOf(section) {
3199
+ const children = section.components().models;
3200
+ return children.length > 0 && children.every((c) => tagOf2(c) === "mj-column") ? children : null;
3201
+ }
3202
+ function ratioWidths(ratio) {
3203
+ if (ratio.every((r) => r === ratio[0])) return null;
3204
+ const total = ratio.reduce((a, b) => a + b, 0);
3205
+ return ratio.map((r) => `${Math.round(r / total * 1e4) / 100}%`);
3206
+ }
3207
+ function currentRatio(columns) {
3208
+ const widths = columns.map((c) => String(c.getAttributes().width ?? ""));
3209
+ return COLUMN_RATIOS[columns.length]?.find((ratio) => {
3210
+ const want = ratioWidths(ratio);
3211
+ if (!want) return widths.every((w) => w === "");
3212
+ return widths.every(
3213
+ (w, i) => /%$/.test(w) && Math.abs(parseFloat(w) - parseFloat(want[i])) < 0.5
3214
+ );
3215
+ }) ?? null;
3216
+ }
3217
+ function setRatio(columns, ratio) {
3218
+ const widths = ratioWidths(ratio);
3219
+ columns.forEach((c, i) => {
3220
+ if (widths) c.addAttributes({ width: widths[i] });
3221
+ else c.removeAttributes(["width"]);
3222
+ });
3223
+ }
3224
+ var PROFILE_URLS = {
3225
+ facebook: "https://facebook.com/",
3226
+ instagram: "https://instagram.com/",
3227
+ x: "https://x.com/",
3228
+ linkedin: "https://linkedin.com/",
3229
+ youtube: "https://youtube.com/",
3230
+ pinterest: "https://pinterest.com/",
3231
+ github: "https://github.com/"
3232
+ };
3233
+ function addNetwork(social) {
3234
+ const present = new Set(
3235
+ social.components().map((c) => String(c.getAttributes().name ?? "").replace(/-noshare$/, ""))
3236
+ );
3237
+ const network = Object.keys(PROFILE_URLS).find((n) => !present.has(n)) ?? "facebook";
3238
+ return social.append(
3239
+ `<mj-social-element name="${network}-noshare" href="${PROFILE_URLS[network]}"></mj-social-element>`
3240
+ )[0];
3241
+ }
3242
+ function addNavbarLink(navbar, text) {
3243
+ return navbar.append(`<mj-navbar-link>${escapeHtml2(text)}</mj-navbar-link>`)[0];
3244
+ }
3245
+ function escapeHtml2(s) {
3246
+ return s.replace(/[&<>"']/g, (ch) => `&#${ch.charCodeAt(0)};`);
3247
+ }
3248
+
3249
+ // src/components/editor/inspector/StructureSection.tsx
3250
+ var import_jsx_runtime15 = require("react/jsx-runtime");
3251
+ function tagOf3(c) {
3252
+ return String(c.get("tagName") || c.get("type") || "");
3253
+ }
3254
+ function StructureSection({
3255
+ editor,
3256
+ component,
3257
+ labels,
3258
+ locked
3259
+ }) {
3260
+ switch (tagOf3(component)) {
3261
+ case "mj-section":
3262
+ return /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(ColumnWidths, { component, labels, locked });
3263
+ case "mj-social":
3264
+ return /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
3265
+ Children,
3266
+ {
3267
+ editor,
3268
+ component,
3269
+ labels,
3270
+ locked,
3271
+ title: labels.socialNetworks,
3272
+ add: labels.addNetwork,
3273
+ icon: /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(import_lucide_react8.AtSign, { className: "size-3.5 shrink-0" }),
3274
+ onAdd: () => editor.select(addNetwork(component))
3275
+ }
3276
+ );
3277
+ case "mj-navbar":
3278
+ return /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
3279
+ Children,
3280
+ {
3281
+ editor,
3282
+ component,
3283
+ labels,
3284
+ locked,
3285
+ title: labels.navbarLinks,
3286
+ add: labels.addLink,
3287
+ icon: /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(import_lucide_react8.Link2, { className: "size-3.5 shrink-0" }),
3288
+ onAdd: () => editor.select(addNavbarLink(component, labels.newLinkText))
3289
+ }
3290
+ );
3291
+ default:
3292
+ return null;
3293
+ }
3294
+ }
3295
+ function Heading({ children }) {
3296
+ return /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("h3", { className: "px-3 pb-1 text-[10px] font-semibold tracking-wider text-panel-muted-fg/80 uppercase", children });
3297
+ }
3298
+ function ColumnWidths({
3299
+ component,
3300
+ labels,
3301
+ locked
3302
+ }) {
3303
+ const columns = columnsOf(component);
3304
+ const ratios = columns && COLUMN_RATIOS[columns.length];
3305
+ if (!columns || !ratios) return null;
3306
+ const current = currentRatio(columns);
3307
+ return /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("section", { className: "border-b border-panel-border/60 py-2", children: [
3308
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(Heading, { children: labels.columnWidths }),
3309
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("div", { className: "flex gap-1.5 px-3 py-1", children: ratios.map((ratio) => {
3310
+ const name = labels.columnRatio(ratio.join(":"));
3311
+ const active = current?.join() === ratio.join();
3312
+ return /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
3313
+ "button",
3314
+ {
3315
+ type: "button",
3316
+ disabled: locked,
3317
+ "aria-pressed": active,
3318
+ title: name,
3319
+ "aria-label": name,
3320
+ onClick: () => !active && setRatio(columns, ratio),
3321
+ className: cn(
3322
+ "flex h-8 flex-1 items-center gap-0.5 rounded-md border px-1.5 transition-colors",
3323
+ active ? "border-brand bg-brand/15" : "border-panel-border bg-panel-elevated hover:border-panel-muted-fg"
3324
+ ),
3325
+ children: ratio.map((part, i) => /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
3326
+ "span",
3327
+ {
3328
+ style: { flexGrow: part },
3329
+ className: cn(
3330
+ "h-4 basis-0 rounded-sm",
3331
+ active ? "bg-brand" : "bg-panel-muted-fg/50"
3332
+ )
3333
+ },
3334
+ i
3335
+ ))
3336
+ },
3337
+ ratio.join()
3338
+ );
3339
+ }) })
3340
+ ] });
3341
+ }
3342
+ function Children({
3343
+ editor,
3344
+ component,
3345
+ labels,
3346
+ locked,
3347
+ title,
3348
+ add,
3349
+ icon,
3350
+ onAdd
3351
+ }) {
3352
+ const children = component.components().models;
3353
+ return /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("section", { className: "border-b border-panel-border/60 py-2", children: [
3354
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(Heading, { children: title }),
3355
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("ul", { className: "flex flex-col px-2", children: children.map((child) => /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("li", { children: /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(
3356
+ "button",
3357
+ {
3358
+ type: "button",
3359
+ onClick: () => editor.select(child),
3360
+ className: "group flex w-full items-center gap-2 rounded-md px-1.5 py-1 text-left text-xs text-panel-fg hover:bg-panel-hover",
3361
+ children: [
3362
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("span", { className: "text-panel-muted-fg", children: icon }),
3363
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("span", { className: "min-w-0 flex-1 truncate", children: layerName(child, labels) }),
3364
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(import_lucide_react8.ChevronRight, { className: "size-3 shrink-0 text-panel-muted-fg opacity-0 group-hover:opacity-100" })
3365
+ ]
3366
+ }
3367
+ ) }, child.cid)) }),
3368
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(
3369
+ "button",
3370
+ {
3371
+ type: "button",
3372
+ disabled: locked,
3373
+ onClick: onAdd,
3374
+ className: "mx-3 mt-1 flex items-center gap-1.5 rounded-md px-1 py-1 text-xs text-panel-muted-fg hover:text-panel-fg disabled:opacity-50",
3375
+ children: [
3376
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(import_lucide_react8.Plus, { className: "size-3.5" }),
3377
+ add
3378
+ ]
3379
+ }
3380
+ )
3381
+ ] });
3382
+ }
3383
+
3384
+ // src/components/editor/inspector/write.ts
3385
+ function same(a, b) {
3386
+ const keys = Object.keys(a);
3387
+ return keys.length === Object.keys(b).length && keys.every((k) => k in b && a[k] === b[k]);
3388
+ }
3389
+ function createWriter(editor, component, blocked = () => false) {
3390
+ const isImage = String(component.get("tagName") || component.get("type")) === "mj-image";
3391
+ const current = () => {
3392
+ const { __p: _marker, style: _style, ...rest } = component.get("attributes");
3393
+ return rest;
3394
+ };
3395
+ let drag = null;
3396
+ const record = (start, end) => {
3397
+ component.setAttributes(start, { avoidStore: true });
3398
+ if (!same(end, start)) component.setAttributes(end);
3399
+ delete component.get("attributes").__p;
3400
+ };
3401
+ return async (attr, value, { partial, remove = [] } = {}) => {
3402
+ if (editor.getEditing() === component) {
3403
+ const view = component.getView();
3404
+ await view?.syncContent?.();
3405
+ }
3406
+ if (blocked()) return;
3407
+ if (drag && drag.attr !== attr) {
3408
+ const { start: start2 } = drag;
3409
+ drag = null;
3410
+ record(start2, current());
3411
+ }
3412
+ if (attr === "src" && isImage) return void component.set({ src: value });
3413
+ const next = current();
3414
+ for (const name of remove) delete next[name];
3415
+ if (value === "") delete next[attr];
3416
+ else next[attr] = value;
3417
+ if (partial) {
3418
+ drag ??= { attr, start: current() };
3419
+ component.setAttributes(next, { avoidStore: true });
3420
+ return;
3421
+ }
3422
+ const start = drag?.start ?? current();
3423
+ drag = null;
3424
+ record(start, next);
3425
+ };
3426
+ }
3427
+
3428
+ // src/components/editor/inspector/Inspector.tsx
3429
+ var import_jsx_runtime16 = require("react/jsx-runtime");
3430
+ var GROUPS = ["content", "layout", "spacing", "style"];
3431
+ var HIDDEN = /* @__PURE__ */ new Set(["css-class", "mj-class", "id", "style"]);
3432
+ function tagOf4(c) {
3433
+ return String(c.get("tagName") || c.get("type") || "");
3434
+ }
3435
+ function attributesOf(c) {
3436
+ const out = {};
3437
+ for (const [k, v] of Object.entries(c.getAttributes())) {
3438
+ if (v !== void 0 && v !== null && v !== false) out[k] = String(v);
3439
+ }
3440
+ if (tagOf4(c) === "mj-image") out.src = String(c.get("src") ?? "");
3441
+ return out;
3442
+ }
3443
+ function documentColors(editor) {
3444
+ const counts = /* @__PURE__ */ new Map();
3445
+ const colorValue = /(?:color|background|border)[\w-]*\s*[:=]\s*["']?[^"';>{}]*?(#[0-9a-f]{3,8}\b|rgba?\([^)]*\))/gi;
3446
+ for (const match of editor.getHtml().matchAll(colorValue)) {
3447
+ const hex = toHex(match[1], { opaque: true });
3448
+ if (hex) counts.set(hex, (counts.get(hex) ?? 0) + 1);
3449
+ }
3450
+ return [...counts.entries()].sort((a, b) => b[1] - a[1]).slice(0, 14).map(([hex]) => hex);
3451
+ }
3452
+ function Inspector({
3453
+ onEmailSettings,
3454
+ empty
3455
+ }) {
3456
+ const editor = (0, import_react8.useEditorMaybe)();
3457
+ const labels = useLabels();
3458
+ const { locked } = useAgentTurn();
3459
+ const lockedNow = (0, import_react7.useRef)(locked);
3460
+ (0, import_react7.useLayoutEffect)(() => {
3461
+ lockedNow.current = locked;
3462
+ });
3463
+ const [, refresh] = (0, import_react7.useReducer)((n) => n + 1, 0);
3464
+ const root = (0, import_react7.useRef)(null);
3465
+ const refocus = (0, import_react7.useRef)(false);
3466
+ (0, import_react7.useEffect)(() => {
3467
+ if (!editor) return;
3468
+ const events = "component:update change:device";
3469
+ const toggled = () => {
3470
+ if (root.current?.contains(document.activeElement)) refocus.current = true;
3471
+ refresh();
3472
+ };
3473
+ editor.on(events, refresh);
3474
+ editor.on("component:toggled", toggled);
3475
+ return () => {
3476
+ editor.off(events, refresh);
3477
+ editor.off("component:toggled", toggled);
3478
+ };
3479
+ }, [editor]);
3480
+ (0, import_react7.useEffect)(() => {
3481
+ if (!refocus.current) return;
3482
+ refocus.current = false;
3483
+ root.current?.querySelector("[data-focus-target]")?.focus();
3484
+ });
3485
+ if (!editor) return null;
3486
+ const selected = editor.getSelectedAll();
3487
+ const component = selected[0];
3488
+ return /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("div", { ref: root, children: selected.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("div", { "data-focus-target": true, tabIndex: -1, className: "outline-none", children: empty }) : selected.length > 1 ? /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
3489
+ "p",
3490
+ {
3491
+ "data-focus-target": true,
3492
+ tabIndex: -1,
3493
+ className: "px-4 py-8 text-center text-xs text-panel-muted-fg outline-none",
3494
+ children: labels.multipleSelected(selected.length)
3495
+ }
3496
+ ) : /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
3497
+ ElementSettings,
3498
+ {
3499
+ editor,
3500
+ component,
3501
+ labels,
3502
+ locked,
3503
+ lockedNow,
3504
+ onEmailSettings
3505
+ },
3506
+ component.cid
3507
+ ) });
3508
+ }
3509
+ function ElementSettings({
3510
+ editor,
3511
+ component,
3512
+ labels,
3513
+ locked,
3514
+ lockedNow,
3515
+ onEmailSettings
3516
+ }) {
3517
+ const [advancedOpen, setAdvancedOpen] = (0, import_react7.useState)(false);
3518
+ const tag = tagOf4(component);
3519
+ const fields = SCHEMA[tag] ?? [];
3520
+ const attributes = attributesOf(component);
3521
+ const visible = fields.filter((f) => !f.when || f.when(attributes));
3522
+ const t = (key) => labels.inspectorLabels[key] ?? key;
3523
+ const optionName = (o) => labels.inspectorLabels[o.label] ?? o.label;
3524
+ const [writer] = (0, import_react7.useState)(() => createWriter(editor, component, () => lockedNow.current));
3525
+ const write = (attr, value, options) => lockedNow.current ? Promise.resolve() : writer(attr, value, options);
3526
+ const writeBox = (attr, shorthand, partial) => write(attr, shorthand, { remove: SIDES.map((s) => `${attr}-${s}`), partial });
3527
+ const chooseImage = (attr) => {
3528
+ if (attr === "src" && tag === "mj-image") return void editor.runCommand("open-image-picker");
3529
+ const am = editor.AssetManager;
3530
+ am.open({
3531
+ types: ["image"],
3532
+ current: imageUrl(attributes[attr] ?? "") ?? void 0,
3533
+ select(asset, complete) {
3534
+ void write(attr, typeof asset === "string" ? asset : asset.getSrc());
3535
+ if (complete) am.close();
3536
+ }
3537
+ });
3538
+ };
3539
+ const safeLink = (attr) => (link) => {
3540
+ if (tag === "mj-navbar-link" && attr === "href") {
3541
+ return (0, import_core2.isSafeNavbarLink)(String(component.parent()?.getAttributes()["base-url"] ?? ""), link);
3542
+ }
3543
+ if (tag === "mj-navbar" && attr === "base-url") {
3544
+ return component.components().every((c) => (0, import_core2.isSafeNavbarLink)(link, String(c.getAttributes().href ?? "")));
3545
+ }
3546
+ return (0, import_core2.isSafeLink)(link);
3547
+ };
3548
+ const control = (f) => {
3549
+ const value = attributes[f.attr] ?? "";
3550
+ const c = f.control;
3551
+ switch (c.type) {
3552
+ case "text":
3553
+ return /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
3554
+ TextInput,
3555
+ {
3556
+ value,
3557
+ placeholder: c.placeholder ?? f.default,
3558
+ onCommit: (v) => write(f.attr, v)
3559
+ }
3560
+ );
3561
+ case "link":
3562
+ return /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
3563
+ TextInput,
3564
+ {
3565
+ value,
3566
+ placeholder: "https://",
3567
+ accept: safeLink(f.attr),
3568
+ onCommit: (v) => write(f.attr, v)
3569
+ }
3570
+ );
3571
+ case "color":
3572
+ return /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
3573
+ ColorField,
3574
+ {
3575
+ value,
3576
+ placeholder: f.default,
3577
+ documentColors: () => documentColors(editor),
3578
+ labels,
3579
+ onChange: (v, partial) => write(f.attr, v, { partial })
3580
+ }
3581
+ );
3582
+ case "image":
3583
+ return /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
3584
+ ImageField,
3585
+ {
3586
+ value,
3587
+ labels,
3588
+ onChoose: () => chooseImage(f.attr),
3589
+ onClear: () => write(f.attr, "")
3590
+ }
3591
+ );
3592
+ case "length":
3593
+ return /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
3594
+ LengthInput,
3595
+ {
3596
+ value,
3597
+ placeholder: f.default,
3598
+ units: c.units,
3599
+ negative: c.negative,
3600
+ labels,
3601
+ onCommit: (v, partial) => write(f.attr, v, { partial })
3602
+ }
3603
+ );
3604
+ case "box":
3605
+ return /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
3606
+ BoxInput,
3607
+ {
3608
+ attributes,
3609
+ attr: f.attr,
3610
+ placeholder: f.default,
3611
+ units: c.units,
3612
+ labels,
3613
+ onCommit: (v, partial) => writeBox(f.attr, v, partial)
3614
+ }
3615
+ );
3616
+ case "segmented":
3617
+ return /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
3618
+ Segmented,
3619
+ {
3620
+ value,
3621
+ fallback: f.default,
3622
+ options: c.options,
3623
+ name: optionName,
3624
+ onCommit: (v) => write(f.attr, v)
3625
+ }
3626
+ );
3627
+ case "select":
3628
+ return /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
3629
+ Select,
3630
+ {
3631
+ value,
3632
+ fallback: f.default,
3633
+ options: c.options,
3634
+ name: optionName,
3635
+ onCommit: (v) => write(f.attr, v)
3636
+ }
3637
+ );
3638
+ case "toggle":
3639
+ return /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
3640
+ Toggle,
3641
+ {
3642
+ checked: value === c.on,
3643
+ label: t(f.label),
3644
+ onCommit: (on) => write(f.attr, on ? c.on : "")
3645
+ }
3646
+ );
3647
+ case "network": {
3648
+ const { network, share } = parseNetwork(value);
3649
+ return /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("div", { className: "flex flex-col gap-1.5", children: [
3650
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
3651
+ Select,
3652
+ {
3653
+ value: network,
3654
+ options: c.networks,
3655
+ name: optionName,
3656
+ onCommit: (n) => write(f.attr, n ? networkName(n, share) : "")
3657
+ }
3658
+ ),
3659
+ network && network !== "custom" && /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
3660
+ Segmented,
3661
+ {
3662
+ value: share ? "share" : "profile",
3663
+ options: [
3664
+ { value: "profile", label: labels.linkTypeProfile },
3665
+ { value: "share", label: labels.linkTypeShare }
3666
+ ],
3667
+ name: (o) => o.label,
3668
+ onCommit: (v) => write(f.attr, networkName(network, v === "share"))
3669
+ }
3670
+ )
3671
+ ] });
3672
+ }
3673
+ }
3674
+ };
3675
+ const own = (f) => f.control.type === "box" ? [f.attr, ...SIDES.map((s) => `${f.attr}-${s}`)].some((a) => attributes[a] !== void 0) : !!attributes[f.attr];
3676
+ const reset = (f) => f.control.type === "box" ? writeBox(f.attr, "") : write(f.attr, "");
3677
+ const wide = (f) => ["color", "image", "link", "text", "box", "network"].includes(f.control.type);
3678
+ const row = (f) => /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
3679
+ Row,
3680
+ {
3681
+ label: t(f.label),
3682
+ help: labels.inspectorLabels[`${f.label}.help`],
3683
+ own: own(f),
3684
+ wide: wide(f),
3685
+ inline: f.control.type === "toggle",
3686
+ labels,
3687
+ onReset: () => void reset(f),
3688
+ children: control(f)
3689
+ },
3690
+ f.attr
3691
+ );
3692
+ const named = new Set(
3693
+ fields.flatMap(
3694
+ (f) => f.control.type === "box" ? [f.attr, ...SIDES.map((s) => `${f.attr}-${s}`)] : [f.attr]
3695
+ )
3696
+ );
3697
+ const others = Object.entries(attributes).filter(([k]) => !named.has(k) && !HIDDEN.has(k));
3698
+ const advanced = visible.filter((f) => f.group === "advanced");
3699
+ const advancedCount = advanced.length + others.length;
3700
+ return /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("div", { className: "pb-6", children: [
3701
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
3702
+ Header,
3703
+ {
3704
+ editor,
3705
+ component,
3706
+ labels,
3707
+ onEmailSettings
3708
+ }
3709
+ ),
3710
+ editor.getDevice() !== "Desktop" && /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("p", { className: "mx-3 mt-2 flex items-center gap-1.5 rounded-md bg-panel-elevated px-2 py-1.5 text-[11px] text-panel-muted-fg", children: [
3711
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(import_lucide_react9.Monitor, { className: "size-3.5 shrink-0" }),
3712
+ labels.allScreenSizes
3713
+ ] }),
3714
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(StructureSection, { editor, component, labels, locked }),
3715
+ GROUPS.map((group) => {
3716
+ const inGroup = visible.filter((f) => f.group === group);
3717
+ if (inGroup.length === 0) return null;
3718
+ return /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("section", { className: "border-b border-panel-border/60 py-2", children: [
3719
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("h3", { className: "px-3 pb-1 text-[10px] font-semibold tracking-wider text-panel-muted-fg/80 uppercase", children: groupName(group, labels) }),
3720
+ inGroup.map(row)
3721
+ ] }, group);
3722
+ }),
3723
+ advancedCount > 0 && /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("section", { className: "py-1", children: [
3724
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)(
3725
+ "button",
3726
+ {
3727
+ type: "button",
3728
+ onClick: () => setAdvancedOpen((o) => !o),
3729
+ "aria-expanded": advancedOpen,
3730
+ className: "flex w-full items-center gap-1 px-3 py-2 text-[10px] font-semibold tracking-wider text-panel-muted-fg/80 uppercase hover:text-panel-fg",
3731
+ children: [
3732
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
3733
+ import_lucide_react9.ChevronRight,
3734
+ {
3735
+ className: cn("size-3 transition-transform", advancedOpen && "rotate-90")
3736
+ }
3737
+ ),
3738
+ labels.groupAdvanced,
3739
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("span", { className: "font-normal normal-case", children: [
3740
+ "(",
3741
+ advancedCount,
3742
+ ")"
3743
+ ] })
3744
+ ]
3745
+ }
3746
+ ),
3747
+ advancedOpen && /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)(import_jsx_runtime16.Fragment, { children: [
3748
+ advanced.map(row),
3749
+ others.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("div", { className: "px-3 pt-2", children: [
3750
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("p", { className: "pb-1 text-[11px] text-panel-muted-fg", children: labels.otherAttributes }),
3751
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("ul", { className: "flex flex-col gap-1", children: others.map(([k, v]) => /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)(
3752
+ "li",
3753
+ {
3754
+ className: "flex items-center gap-2 rounded-md bg-panel-elevated px-2 py-1 font-mono text-[11px]",
3755
+ children: [
3756
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("span", { className: "text-panel-muted-fg", children: k }),
3757
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("span", { className: "min-w-0 flex-1 truncate text-panel-fg", title: v, children: v }),
3758
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
3759
+ "button",
3760
+ {
3761
+ type: "button",
3762
+ onClick: () => void write(k, ""),
3763
+ title: labels.removeAttribute,
3764
+ "aria-label": `${labels.removeAttribute}: ${k}`,
3765
+ className: "text-panel-muted-fg hover:text-panel-fg",
3766
+ children: /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(import_lucide_react9.X, { className: "size-3" })
3767
+ }
3768
+ )
3769
+ ]
3770
+ },
3771
+ k
3772
+ )) })
3773
+ ] })
3774
+ ] })
3775
+ ] })
3776
+ ] });
3777
+ }
3778
+ function groupName(group, labels) {
3779
+ return {
3780
+ content: labels.groupContent,
3781
+ layout: labels.groupLayout,
3782
+ spacing: labels.groupSpacing,
3783
+ style: labels.groupStyle
3784
+ }[group];
3785
+ }
3786
+ function Header({
3787
+ editor,
3788
+ component,
3789
+ labels,
3790
+ onEmailSettings
3791
+ }) {
3792
+ const tag = tagOf4(component);
3793
+ const isBody = tag === "mj-body";
3794
+ const name = isBody ? labels.emailSettings : labels.elementNames[tag] ?? tag.replace(/^mj-/, "");
3795
+ const words = tag === "mj-column" ? "" : layerName(component, labels);
3796
+ const excerpt = words === name ? "" : words;
3797
+ const parent = component.parent();
3798
+ const action = "flex size-7 items-center justify-center rounded-md text-panel-muted-fg hover:bg-panel-hover hover:text-panel-fg";
3799
+ return /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("div", { className: "sticky top-0 z-10 flex items-center gap-2 border-b border-panel-border bg-panel px-3 py-2", children: [
3800
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("div", { className: "min-w-0 flex-1", children: [
3801
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
3802
+ "p",
3803
+ {
3804
+ "data-focus-target": true,
3805
+ tabIndex: -1,
3806
+ className: "truncate text-sm font-medium text-panel-fg outline-none",
3807
+ children: name
3808
+ }
3809
+ ),
3810
+ excerpt && !isBody && /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("p", { className: "truncate text-[11px] text-panel-muted-fg", children: [
3811
+ "\u201C",
3812
+ excerpt,
3813
+ "\u201D"
3814
+ ] })
3815
+ ] }),
3816
+ !isBody && parent && /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
3817
+ "button",
3818
+ {
3819
+ type: "button",
3820
+ className: action,
3821
+ title: labels.selectParent,
3822
+ "aria-label": labels.selectParent,
3823
+ onClick: () => tagOf4(parent) === "mj-body" ? onEmailSettings() : editor.select(parent),
3824
+ children: /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(import_lucide_react9.ArrowUp, { className: "size-3.5" })
3825
+ }
3826
+ ),
3827
+ !isBody && /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)(import_jsx_runtime16.Fragment, { children: [
3828
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
3829
+ "button",
3830
+ {
3831
+ type: "button",
3832
+ className: action,
3833
+ title: labels.duplicate,
3834
+ "aria-label": labels.duplicate,
3835
+ onClick: () => editor.runCommand("tlb-clone"),
3836
+ children: /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(import_lucide_react9.Copy, { className: "size-3.5" })
3837
+ }
3838
+ ),
3839
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
1247
3840
  "button",
1248
3841
  {
1249
3842
  type: "button",
1250
- title: transparentLabel,
1251
- onClick: () => onPick("transparent"),
1252
- className: "relative aspect-square overflow-hidden rounded border border-white/15 outline-none hover:scale-110 focus-visible:ring-2 focus-visible:ring-brand",
1253
- children: /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(Checkerboard, {})
3843
+ className: action,
3844
+ title: labels.deleteElement,
3845
+ "aria-label": labels.deleteElement,
3846
+ onClick: () => editor.runCommand("tlb-delete"),
3847
+ children: /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(import_lucide_react9.Trash2, { className: "size-3.5" })
1254
3848
  }
1255
3849
  )
1256
3850
  ] })
1257
3851
  ] });
1258
3852
  }
1259
- function Checkerboard() {
1260
- return /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
1261
- "span",
1262
- {
1263
- className: "absolute inset-0",
1264
- style: {
1265
- backgroundImage: "repeating-conic-gradient(#9ca3af 0% 25%, #e5e7eb 0% 50%)",
1266
- backgroundSize: "8px 8px"
1267
- }
3853
+
3854
+ // src/components/editor/blocks.ts
3855
+ var import_grapesjs = __toESM(require("grapesjs"), 1);
3856
+ var import_grapesjs_mjml = __toESM(require("grapesjs-mjml"), 1);
3857
+
3858
+ // src/components/editor/ids.ts
3859
+ var ID_RE = {
3860
+ sec: /(?:^|\s)sec-([A-Za-z0-9_-]+)(?=\s|$)/,
3861
+ obj: /(?:^|\s)obj-([A-Za-z0-9_-]+)(?=\s|$)/
3862
+ };
3863
+ function idOf(c, prefix) {
3864
+ return String(c.getAttributes()["css-class"] ?? "").match(ID_RE[prefix])?.[1] ?? null;
3865
+ }
3866
+ var newId = () => Math.random().toString(36).slice(2, 10);
3867
+ function sectionIdOf(c) {
3868
+ return idOf(c, "sec");
3869
+ }
3870
+ function addIdClass(c, prefix) {
3871
+ const existing = idOf(c, prefix);
3872
+ if (existing) return existing;
3873
+ const cssClass = String(c.getAttributes()["css-class"] ?? "");
3874
+ const id = newId();
3875
+ c.addAttributes({ "css-class": `${cssClass} ${prefix}-${id}`.trim() });
3876
+ return id;
3877
+ }
3878
+ function freshenCopiedIds(root, added) {
3879
+ const used = (prefix, id, except) => {
3880
+ let found = false;
3881
+ root?.onAll((o) => {
3882
+ if (o !== except && idOf(o, prefix) === id) found = true;
3883
+ });
3884
+ return found;
3885
+ };
3886
+ added.onAll((c) => {
3887
+ for (const prefix of ["sec", "obj"]) {
3888
+ const id = idOf(c, prefix);
3889
+ if (!id || !used(prefix, id, c)) continue;
3890
+ const cssClass = String(c.getAttributes()["css-class"]);
3891
+ const fresh = cssClass.replace(
3892
+ ID_RE[prefix],
3893
+ (token) => token.replace(`${prefix}-${id}`, `${prefix}-${newId()}`)
3894
+ );
3895
+ c.addAttributes({ "css-class": fresh });
1268
3896
  }
1269
- );
3897
+ });
1270
3898
  }
1271
- function toHex(value, { opaque = false } = {}) {
1272
- const v = value.trim().toLowerCase();
1273
- const short = /^#([0-9a-f])([0-9a-f])([0-9a-f])$/.exec(v);
1274
- if (short) return `#${short[1]}${short[1]}${short[2]}${short[2]}${short[3]}${short[3]}`;
1275
- if (/^#[0-9a-f]{6}$/.test(v)) return v;
1276
- if (/^#[0-9a-f]{8}$/.test(v)) return opaque || v.endsWith("ff") ? v.slice(0, 7) : v;
1277
- const rgb = /^rgba?\(\s*(\d+)[\s,]+(\d+)[\s,]+(\d+)(?:[\s,/]+([\d.]+%?))?/.exec(v);
1278
- if (rgb) {
1279
- const channels = [rgb[1], rgb[2], rgb[3]].map((n) => Math.min(255, Number(n)));
1280
- const alphaText = rgb[4];
1281
- const alpha = alphaText?.endsWith("%") ? parseFloat(alphaText) / 100 : Number(alphaText ?? 1);
1282
- if (!opaque && alpha < 1) channels.push(Math.round(Math.max(0, alpha) * 255));
1283
- return `#${channels.map((n) => n.toString(16).padStart(2, "0")).join("")}`;
3899
+
3900
+ // src/components/editor/insert-block.ts
3901
+ var addsBlock = (e) => (e.key === "Enter" || e.key === " ") && !e.repeat && !e.ctrlKey && !e.metaKey && !e.altKey;
3902
+ var tileAttributes = (id) => ({ tabindex: "0", role: "button", "data-block": id });
3903
+ var tagOf5 = (c) => String(c.get("tagName") ?? "");
3904
+ function closest(c, tag) {
3905
+ for (let cur = c; cur; cur = cur.parent() ?? void 0) if (tagOf5(cur) === tag) return cur;
3906
+ return void 0;
3907
+ }
3908
+ function childOf(parent, c) {
3909
+ let cur = c;
3910
+ while (cur.parent() && cur.parent() !== parent) cur = cur.parent();
3911
+ return cur;
3912
+ }
3913
+ function insertBlock(editor, block) {
3914
+ const content = block.getContent();
3915
+ let body;
3916
+ editor.getWrapper()?.onAll((c) => {
3917
+ if (!body && tagOf5(c) === "mj-body") body = c;
3918
+ });
3919
+ if (typeof content !== "string" || !body) return void 0;
3920
+ const selected = editor.getSelected();
3921
+ const section = closest(selected, "mj-section");
3922
+ const sections = section?.parent() ?? body;
3923
+ const afterSection = section ? { at: section.index() + 1 } : {};
3924
+ const column = closest(selected, "mj-column");
3925
+ let added;
3926
+ if (/^\s*<mj-section\b/i.test(content)) {
3927
+ added = sections.append(content, afterSection)[0];
3928
+ } else if (column && selected) {
3929
+ const at = selected === column ? {} : { at: childOf(column, selected).index() + 1 };
3930
+ added = column.append(content, at)[0];
3931
+ } else {
3932
+ const [wrapper] = sections.append(
3933
+ `<mj-section><mj-column>${content}</mj-column></mj-section>`,
3934
+ afterSection
3935
+ );
3936
+ added = wrapper?.components().at(0)?.components().at(0);
1284
3937
  }
1285
- return null;
3938
+ if (!added) return void 0;
3939
+ editor.select(added);
3940
+ if (block.get("activate")) added.getView()?.onActive(void 0);
3941
+ return added;
1286
3942
  }
1287
3943
 
1288
- // src/components/editor/ImageField.tsx
1289
- var import_lucide_react6 = require("lucide-react");
1290
- var import_jsx_runtime12 = require("react/jsx-runtime");
1291
- function ImageField({
1292
- value,
1293
- labels,
1294
- onChoose,
1295
- onClear
1296
- }) {
1297
- const url = imageUrl(value);
1298
- return /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { className: "relative", children: [
1299
- /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(
1300
- "button",
1301
- {
1302
- type: "button",
1303
- onClick: onChoose,
1304
- title: labels.chooseImage,
1305
- className: "group relative block w-full overflow-hidden rounded-md border border-panel-border bg-panel-elevated outline-none focus-visible:ring-2 focus-visible:ring-brand",
1306
- children: [
1307
- url ? /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("img", { src: url, alt: "", className: "h-24 w-full bg-black/25 object-contain" }) : /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("span", { className: "flex h-16 flex-col items-center justify-center gap-1 text-xs text-panel-muted-fg", children: [
1308
- /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(import_lucide_react6.ImageIcon, { className: "size-5" }),
1309
- labels.chooseImage
1310
- ] }),
1311
- url && /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("span", { className: "absolute inset-0 flex items-center justify-center bg-black/50 text-xs font-medium text-white opacity-0 transition group-hover:opacity-100", children: labels.chooseImage })
1312
- ]
3944
+ // src/components/editor/blocks.ts
3945
+ var EDITABLE_TAGS = /* @__PURE__ */ new Set([
3946
+ "mj-section",
3947
+ "mj-group",
3948
+ "mj-column",
3949
+ "mj-text",
3950
+ "mj-image",
3951
+ "mj-button",
3952
+ "mj-divider",
3953
+ "mj-spacer",
3954
+ "mj-social",
3955
+ "mj-social-element",
3956
+ "mj-navbar",
3957
+ "mj-navbar-link"
3958
+ ]);
3959
+ var CONTENT_TAGS = /* @__PURE__ */ new Set(["mj-text", "mj-button", "mj-social-element", "mj-navbar-link"]);
3960
+ var EDITOR_INTERNALS = /data-gjs-|__p\b|custom-name/i;
3961
+ var DROPPED_MARKUP = /<(?:script|style|link|meta|iframe|object|embed|form)\b/i;
3962
+ var INLINE_IMAGE = /\b(?:src|srcset|background-url|icon)\s*=\s*(?:"[^"]*data:|'[^']*data:|data:)/i;
3963
+ var LINK_IN_MARKUP = /[\s/"'](?:xlink:)?(?:href|action|formaction)\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s>]+))/gi;
3964
+ var validator;
3965
+ function blockProblems(mjml, editor = validator ??= import_grapesjs.default.init({
3966
+ headless: true,
3967
+ storageManager: false,
3968
+ telemetry: false,
3969
+ plugins: [import_grapesjs_mjml.default]
3970
+ })) {
3971
+ if (EDITOR_INTERNALS.test(mjml)) return ["it contains GrapesJS's own attributes"];
3972
+ if (INLINE_IMAGE.test(mjml)) return ["it inlines an image as a data: URL"];
3973
+ let tree;
3974
+ try {
3975
+ tree = editor.runCommand("mjml-code-to-html", {
3976
+ mjml: `<mjml><mj-body>${withClosedTags(mjml)}</mj-body></mjml>`,
3977
+ validationLevel: "strict"
3978
+ }).json;
3979
+ } catch (e) {
3980
+ const errors = e.errors;
3981
+ if (!errors) throw e;
3982
+ return errors.map((error) => error.formattedMessage ?? error.message ?? String(error));
3983
+ }
3984
+ const problems = [];
3985
+ const top = tree.children?.find((c) => c.tagName === "mj-body")?.children ?? [];
3986
+ if (top.length !== 1 || top[0].tagName !== "mj-section") {
3987
+ problems.push("it is not exactly one mj-section");
3988
+ }
3989
+ const visit = (node, parent) => {
3990
+ const tag = node.tagName;
3991
+ const attributes = node.attributes ?? {};
3992
+ const content = node.content ?? "";
3993
+ if (!EDITABLE_TAGS.has(tag)) problems.push(`${tag} is not editable here`);
3994
+ const cssClass = attributes["css-class"] ?? "";
3995
+ if (ID_RE.sec.test(cssClass) || ID_RE.obj.test(cssClass)) {
3996
+ problems.push(`${tag} carries a sec-* or obj-* id`);
3997
+ }
3998
+ if ("mj-class" in attributes) problems.push(`${tag} uses mj-class`);
3999
+ const links = [attributes.href ?? ""];
4000
+ for (const match of content.matchAll(LINK_IN_MARKUP)) {
4001
+ links.push(match[1] ?? match[2] ?? match[3] ?? "");
4002
+ }
4003
+ const linked = tag === "mj-navbar-link" ? (0, import_core2.isSafeNavbarLink)(parent?.attributes?.["base-url"] ?? "", attributes.href ?? "") : true;
4004
+ if (!linked || !links.every(import_core2.isSafeLink)) {
4005
+ problems.push(`${tag} has a link scheme mail must not carry`);
4006
+ }
4007
+ if (CONTENT_TAGS.has(tag) ? /<mj-/i.test(content) || DROPPED_MARKUP.test(content) : content.trim()) {
4008
+ problems.push(`${tag} holds content the editor does not keep`);
4009
+ }
4010
+ node.children?.forEach((child) => visit(child, node));
4011
+ };
4012
+ top.forEach((node) => visit(node));
4013
+ return problems;
4014
+ }
4015
+ var escapeHtml3 = (text) => text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
4016
+ function registerBlocks(editor, blocks, category, media) {
4017
+ for (const block of blocks) {
4018
+ const problems = editor.Blocks.get(block.id) ? [`the id is already taken`] : blockProblems(block.mjml, editor);
4019
+ if (problems.length > 0) {
4020
+ console.error(`Block "${block.id}" is left out of the palette: ${problems.join("; ")}`);
4021
+ continue;
4022
+ }
4023
+ editor.Blocks.add(block.id, {
4024
+ // GrapesJS writes the label into the tile as markup.
4025
+ label: escapeHtml3(block.label),
4026
+ category,
4027
+ media: block.media ?? media,
4028
+ content: withClosedTags(block.mjml),
4029
+ attributes: tileAttributes(block.id)
4030
+ });
4031
+ }
4032
+ }
4033
+
4034
+ // src/components/editor/builtin-blocks.ts
4035
+ var BUILTIN_BLOCKS = [
4036
+ "mj-1-column",
4037
+ "mj-2-columns",
4038
+ "mj-3-columns",
4039
+ "mj-text",
4040
+ "mj-image",
4041
+ "mj-button",
4042
+ "mj-divider",
4043
+ "mj-spacer",
4044
+ "mj-social-group",
4045
+ "mj-navbar"
4046
+ ];
4047
+ var LAYOUT_BLOCKS = /* @__PURE__ */ new Set(["mj-1-column", "mj-2-columns", "mj-3-columns"]);
4048
+ var CONTENT2 = {
4049
+ // The plugin's version links Google+ and shares the page: a bare network name makes the
4050
+ // icon open a "share this link" dialog, while `-noshare` links to the profile a footer
4051
+ // means. Without `alt` each link is an unnamed image to a screen reader.
4052
+ "mj-social-group": `<mj-social icon-size="24px" mode="horizontal">
4053
+ <mj-social-element name="facebook-noshare" href="https://facebook.com/" alt="Facebook"></mj-social-element>
4054
+ <mj-social-element name="instagram-noshare" href="https://instagram.com/" alt="Instagram"></mj-social-element>
4055
+ <mj-social-element name="x-noshare" href="https://x.com/" alt="X"></mj-social-element>
4056
+ </mj-social>`,
4057
+ // The plugin's sample is a via.placeholder.com picture, stored in the email and loaded by
4058
+ // every reader's mail client. Dropping one opens the image picker instead.
4059
+ "mj-image": "<mj-image></mj-image>"
4060
+ };
4061
+ function builtinBlock(labels) {
4062
+ return (id) => ({
4063
+ label: labels.blockLabels[id] ?? id,
4064
+ // By id: a regex on the label broke as soon as a host translated it.
4065
+ category: LAYOUT_BLOCKS.has(id) ? labels.blockCategoryLayout : labels.blockCategoryContent,
4066
+ ...id in CONTENT2 && { content: CONTENT2[id] },
4067
+ attributes: tileAttributes(id)
4068
+ });
4069
+ }
4070
+ var CANVAS_CSS = 'img:not([src]), img[src=""] { min-height: 120px; background: #f4f4f5; outline: 1px dashed #a1a1aa; outline-offset: -1px; }';
4071
+
4072
+ // src/components/editor/table-type.ts
4073
+ var import_core3 = require("@mjml-agent-editor/core");
4074
+ var DROPPED = "script, style, link, iframe, frame, object, embed, applet, base, meta, animate, set, svg, math, template";
4075
+ var LINK_ATTRIBUTES = /* @__PURE__ */ new Set(["href", "xlink:href", "action", "formaction"]);
4076
+ function cleanRows(rows) {
4077
+ rows.querySelectorAll(DROPPED).forEach((el) => el.remove());
4078
+ rows.querySelectorAll("*").forEach((el) => {
4079
+ for (const { name, value } of [...el.attributes]) {
4080
+ const lower = name.toLowerCase();
4081
+ const script = /^(java|vb)script:/i.test(value.replace(/[\u0000-\u0020]/g, ""));
4082
+ if (lower.startsWith("on") || script || LINK_ATTRIBUTES.has(lower) && !(0, import_core3.isSafeLink)(value)) {
4083
+ el.removeAttribute(name);
1313
4084
  }
1314
- ),
1315
- value && /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
1316
- "button",
1317
- {
1318
- type: "button",
1319
- onClick: onClear,
1320
- title: labels.imageFieldRemove,
1321
- "aria-label": labels.imageFieldRemove,
1322
- className: "absolute top-1 right-1 flex size-5 items-center justify-center rounded bg-black/60 text-white hover:bg-black/80",
1323
- children: /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(import_lucide_react6.X, { className: "size-3" })
4085
+ }
4086
+ });
4087
+ }
4088
+ var ROW_STYLE = "pointer-events: all; display: table; width: 100%;";
4089
+ function tablePreview(doc, attributes, rows) {
4090
+ const attr = (name, fallback) => String(attributes[name] ?? fallback);
4091
+ const template = doc.createElement("template");
4092
+ template.innerHTML = `<table>${rows}</table>`;
4093
+ const table = doc.createElement("table");
4094
+ table.setAttribute("cellpadding", String(parseInt(attr("cellpadding", "0"), 10) || 0));
4095
+ table.setAttribute("cellspacing", String(parseInt(attr("cellspacing", "0"), 10) || 0));
4096
+ Object.assign(table.style, {
4097
+ width: attr("width", "100%"),
4098
+ border: attr("border", "none"),
4099
+ color: attr("color", "#000000"),
4100
+ fontFamily: attr("font-family", "Ubuntu, Helvetica, Arial, sans-serif"),
4101
+ fontSize: attr("font-size", "13px"),
4102
+ lineHeight: attr("line-height", "22px"),
4103
+ tableLayout: attr("table-layout", "auto")
4104
+ });
4105
+ for (const row of template.content.querySelectorAll("tr")) {
4106
+ const tr = table.insertRow();
4107
+ for (const cell2 of row.querySelectorAll(":scope > th, :scope > td")) {
4108
+ const copy = doc.createElement(cell2.tagName.toLowerCase());
4109
+ copy.textContent = cell2.textContent;
4110
+ const align = cell2.getAttribute("align");
4111
+ if (align && /^(left|right|center)$/i.test(align)) copy.setAttribute("align", align);
4112
+ tr.append(copy);
4113
+ }
4114
+ }
4115
+ const cell = doc.createElement("td");
4116
+ cell.style.padding = attr("padding", "10px 25px");
4117
+ cell.append(table);
4118
+ return cell;
4119
+ }
4120
+ function addTableType(editor) {
4121
+ const base = editor.Components.getType("default").view.prototype;
4122
+ editor.Components.addType("mj-table", {
4123
+ isComponent: (el) => {
4124
+ if (el.tagName?.toLowerCase() !== "mj-table") return false;
4125
+ const rows = el.querySelector(":scope > template");
4126
+ if (rows) cleanRows(rows.content);
4127
+ return { type: "mj-table", content: rows?.innerHTML ?? "", components: [] };
4128
+ },
4129
+ model: { defaults: { tagName: "mj-table", droppable: false, editable: false } },
4130
+ view: {
4131
+ tagName: "tr",
4132
+ // GrapesJS clears the element's attributes, style among them, when the model's change.
4133
+ updateAttributes(...args) {
4134
+ base.updateAttributes.apply(this, args);
4135
+ this.el.style.cssText = ROW_STYLE;
4136
+ },
4137
+ updateContent() {
4138
+ const { el, model } = this;
4139
+ el.style.cssText = ROW_STYLE;
4140
+ el.replaceChildren(
4141
+ tablePreview(el.ownerDocument, model.getAttributes(), String(model.get("content") ?? ""))
4142
+ );
1324
4143
  }
1325
- )
1326
- ] });
4144
+ }
4145
+ });
1327
4146
  }
1328
- function imageUrl(value) {
1329
- const v = value.trim();
1330
- if (!v || v === "none") return null;
1331
- const css = /^url\(\s*(['"]?)(.*?)\1\s*\)$/i.exec(v);
1332
- if (css) return css[2] || null;
1333
- return /^[a-z-]+\(/i.test(v) ? null : v;
4147
+
4148
+ // src/components/editor/plugin-types.ts
4149
+ var COLUMN_CHILDREN = /* @__PURE__ */ new Set([
4150
+ "mj-accordion",
4151
+ "mj-button",
4152
+ "mj-carousel",
4153
+ "mj-divider",
4154
+ "mj-image",
4155
+ "mj-navbar",
4156
+ "mj-raw",
4157
+ "mj-social",
4158
+ "mj-spacer",
4159
+ "mj-table",
4160
+ "mj-text"
4161
+ ]);
4162
+ function fixPluginTypes(editor) {
4163
+ addTableType(editor);
4164
+ for (const type of editor.Components.getTypes()) {
4165
+ if (!type.id.startsWith("mj-") || type.id === "mj-table") continue;
4166
+ const base = type.model.prototype.getAttrToHTML;
4167
+ editor.Components.addType(type.id, {
4168
+ model: {
4169
+ defaults: {
4170
+ "style-default": {},
4171
+ ...type.id === "mj-column" && {
4172
+ droppable: (source) => COLUMN_CHILDREN.has(String(source.get("tagName")))
4173
+ },
4174
+ // GrapesJS's own is an SVG's markup, which the canvas wrote into `src` and saved.
4175
+ ...type.id === "mj-image" && { src: "" }
4176
+ },
4177
+ getAttrToHTML() {
4178
+ const attributes = base.call(this);
4179
+ delete attributes.__p;
4180
+ for (const [name, value] of Object.entries(attributes)) {
4181
+ if (typeof value === "string") attributes[name] = value.replace(/"/g, "&quot;");
4182
+ }
4183
+ return attributes;
4184
+ }
4185
+ }
4186
+ });
4187
+ }
1334
4188
  }
1335
4189
 
1336
4190
  // src/components/editor/EmailEditor.tsx
1337
- var import_client = require("react-dom/client");
1338
- var import_jsx_runtime13 = require("react/jsx-runtime");
1339
- var SEC_ID_RE = /\bsec-([A-Za-z0-9_-]+)\b/;
1340
- var OBJ_ID_RE = /\bobj-([A-Za-z0-9_-]+)\b/;
4191
+ var import_jsx_runtime17 = require("react/jsx-runtime");
1341
4192
  var TYPE_LABEL = {
1342
4193
  "mj-text": "Text",
1343
4194
  "mj-button": "Button",
1344
4195
  "mj-image": "Image",
1345
4196
  "mj-column": "Column"
1346
4197
  };
1347
- function tagOf(c) {
4198
+ function tagOf6(c) {
1348
4199
  return String(c.get("tagName") || c.get("type") || "");
1349
4200
  }
1350
4201
  var SPARKLES_SVG = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9.937 15.5A2 2 0 0 0 8.5 14.063l-6.135-1.582a.5.5 0 0 1 0-.962L8.5 9.936A2 2 0 0 0 9.937 8.5l1.582-6.135a.5.5 0 0 1 .963 0L14.063 8.5A2 2 0 0 0 15.5 9.937l6.135 1.581a.5.5 0 0 1 0 .964L15.5 14.063a2 2 0 0 0-1.437 1.437l-1.582 6.135a.5.5 0 0 1-.963 0z"/><path d="M20 3v4"/><path d="M22 5h-4"/><path d="M4 17v2"/><path d="M5 18H3"/></svg>';
1351
4202
  var IMAGE_SVG = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10.3 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2v10l-3.1-3.1a2 2 0 0 0-2.814.014L6 21"/><path d="m14 19.5 3-3 3 3"/><path d="M17 22v-5.5"/><circle cx="9" cy="9" r="2"/></svg>';
4203
+ var SECTION_SVG = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="18" height="7" x="3" y="3" rx="1"/><rect width="9" height="7" x="3" y="14" rx="1"/><rect width="5" height="7" x="16" y="14" rx="1"/></svg>';
1352
4204
  function isSection(c) {
1353
- return tagOf(c) === "mj-section";
4205
+ return tagOf6(c) === "mj-section";
1354
4206
  }
1355
4207
  function isCommentable(c) {
1356
- const t = tagOf(c);
4208
+ const t = tagOf6(c);
1357
4209
  return t === "mj-section" || t in TYPE_LABEL;
1358
4210
  }
1359
- function classMatch(c, re) {
1360
- const cssClass = String(c.getAttributes()["css-class"] ?? "");
1361
- return cssClass.match(re)?.[1] ?? null;
1362
- }
1363
- function sectionIdOf(c) {
1364
- return classMatch(c, SEC_ID_RE);
1365
- }
1366
- function addIdClass(c, prefix) {
1367
- const existing = classMatch(c, prefix === "sec" ? SEC_ID_RE : OBJ_ID_RE);
1368
- if (existing) return existing;
1369
- const cssClass = String(c.getAttributes()["css-class"] ?? "");
1370
- const id = Math.random().toString(36).slice(2, 10);
1371
- c.addAttributes({ "css-class": `${cssClass} ${prefix}-${id}`.trim() });
1372
- return id;
1373
- }
1374
4211
  function closestSection(c) {
1375
4212
  let cur = c;
1376
4213
  while (cur) {
@@ -1379,27 +4216,12 @@ function closestSection(c) {
1379
4216
  }
1380
4217
  return null;
1381
4218
  }
1382
- var CRUMB_NAME = {
1383
- "mj-section": "Section",
1384
- "mj-column": "Column",
1385
- "mj-group": "Group",
1386
- "mj-text": "Text",
1387
- "mj-button": "Button",
1388
- "mj-image": "Image",
1389
- "mj-divider": "Divider",
1390
- "mj-spacer": "Spacer",
1391
- "mj-hero": "Hero",
1392
- "mj-navbar": "Navbar",
1393
- "mj-social": "Social",
1394
- "mj-social-element": "Social",
1395
- "mj-wrapper": "Wrapper"
1396
- };
1397
- function crumbName(c) {
1398
- const t = tagOf(c);
1399
- return CRUMB_NAME[t] ?? t.replace(/^mj-/, "") ?? "Element";
4219
+ function crumbName(c, labels) {
4220
+ const t = tagOf6(c);
4221
+ return labels.elementNames[t] ?? t.replace(/^mj-/, "");
1400
4222
  }
1401
4223
  function objectLabel(c) {
1402
- const base = TYPE_LABEL[tagOf(c)] ?? tagOf(c);
4224
+ const base = TYPE_LABEL[tagOf6(c)] ?? tagOf6(c);
1403
4225
  const text = String(c.getEl?.()?.textContent ?? "").replace(/\s+/g, " ").trim().slice(0, 30);
1404
4226
  return text ? `${base}: \u201C${text}\u201D` : base;
1405
4227
  }
@@ -1431,75 +4253,6 @@ function wrapSelectionStyle(el, style) {
1431
4253
  } catch {
1432
4254
  }
1433
4255
  }
1434
- function setupStyleFields(editor, labels) {
1435
- const sm = editor.StyleManager;
1436
- const base = sm.getType("base").view;
1437
- const documentColors = () => {
1438
- const source = `${editor.getHtml()} ${editor.getCss() ?? ""}`;
1439
- const counts = /* @__PURE__ */ new Map();
1440
- const colorValue = /(?:color|background|border)[\w-]*\s*[:=]\s*["']?[^"';>{}]*?(#[0-9a-f]{3,8}\b|rgba?\([^)]*\))/gi;
1441
- for (const match of source.matchAll(colorValue)) {
1442
- const hex = toHex(match[1], { opaque: true });
1443
- if (hex) counts.set(hex, (counts.get(hex) ?? 0) + 1);
1444
- }
1445
- return [...counts.entries()].sort((a, b) => b[1] - a[1]).slice(0, 14).map(([hex]) => hex);
1446
- };
1447
- const ownValue = (property) => property.hasValue?.({ noParent: true }) ? String(property.getValue?.() ?? "") : "";
1448
- const reactView = (field) => base.extend({
1449
- create({ property }) {
1450
- const el = document.createElement("div");
1451
- el.addEventListener("change", (event) => event.stopPropagation());
1452
- const root = (0, import_client.createRoot)(el);
1453
- this.__root = root;
1454
- this.__render = () => root.render(field(property, ownValue(property)));
1455
- this.__render();
1456
- return el;
1457
- },
1458
- update() {
1459
- this.__render?.();
1460
- },
1461
- destroy() {
1462
- const root = this.__root;
1463
- this.__root = void 0;
1464
- this.__render = void 0;
1465
- setTimeout(() => root?.unmount());
1466
- }
1467
- });
1468
- sm.addType("color", {
1469
- view: reactView((property, value) => /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
1470
- ColorField,
1471
- {
1472
- value,
1473
- placeholder: String(property.getDefaultValue?.() ?? ""),
1474
- documentColors,
1475
- labels,
1476
- onChange: (next, partial) => property.upValue(next, { partial })
1477
- }
1478
- ))
1479
- });
1480
- sm.addType("file", {
1481
- view: reactView((property, value) => /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
1482
- ImageField,
1483
- {
1484
- value,
1485
- labels,
1486
- onClear: () => property.upValue(""),
1487
- onChoose: () => {
1488
- const am = editor.AssetManager;
1489
- am.open({
1490
- types: ["image"],
1491
- current: imageUrl(value) ?? void 0,
1492
- select(asset, complete) {
1493
- const url = typeof asset === "string" ? asset : asset.getSrc();
1494
- property.upValue(url, { partial: !complete });
1495
- if (complete) am.close();
1496
- }
1497
- });
1498
- }
1499
- }
1500
- ))
1501
- });
1502
- }
1503
4256
  function setupRichText(editor, labels) {
1504
4257
  const rte = editor.RichTextEditor;
1505
4258
  if (!rte || rte.__customActions) return;
@@ -1536,26 +4289,45 @@ function setupRichText(editor, labels) {
1536
4289
  result: (r, action) => r.exec("hiliteColor", val(action, "input"))
1537
4290
  });
1538
4291
  }
1539
- function EmailEditor({ docId, onReady, commentsRefresh, onOpenCountChange }) {
4292
+ function EmailEditor({
4293
+ docId,
4294
+ onReady,
4295
+ commentsRefresh,
4296
+ onOpenCountChange,
4297
+ blocks = []
4298
+ }) {
1540
4299
  const documents = useDocumentStore();
1541
4300
  const labels = useLabels();
1542
- const loadingRef = (0, import_react7.useRef)(false);
1543
- const saveStatusRef = (0, import_react7.useRef)("idle");
1544
- const stateListeners = (0, import_react7.useRef)(/* @__PURE__ */ new Set());
1545
- const [loading, setLoading] = (0, import_react7.useState)(true);
1546
- const [sidebarView, setSidebarView] = (0, import_react7.useState)("blocks");
1547
- const [composeTarget, setComposeTarget] = (0, import_react7.useState)(null);
1548
- const selectionSource = (0, import_react7.useRef)("other");
1549
- const editorRef = (0, import_react7.useRef)(null);
4301
+ const loadingRef = (0, import_react10.useRef)(false);
4302
+ const saveStatusRef = (0, import_react10.useRef)("idle");
4303
+ const stateListeners = (0, import_react10.useRef)(/* @__PURE__ */ new Set());
4304
+ const [loading, setLoading] = (0, import_react10.useState)(true);
4305
+ const [sidebarView, setSidebarView] = (0, import_react10.useState)("blocks");
4306
+ const blockSearchRef = (0, import_react10.useRef)(null);
4307
+ const onBlockSearchKey = (e) => {
4308
+ if (readOnlyRef.current || !opensBlockSearch(e)) return;
4309
+ e.preventDefault();
4310
+ setSidebarView("blocks");
4311
+ requestAnimationFrame(() => blockSearchRef.current?.focus());
4312
+ };
4313
+ const [composeTarget, setComposeTarget] = (0, import_react10.useState)(null);
4314
+ const selectionSource = (0, import_react10.useRef)("other");
4315
+ const editorRef = (0, import_react10.useRef)(null);
1550
4316
  const { running: turnRunning, locked: readOnly, setLocked } = useAgentTurn();
1551
- const readOnlyRef = (0, import_react7.useRef)(false);
1552
- const applyReadOnly = (0, import_react7.useRef)(async () => {
4317
+ const readOnlyRef = (0, import_react10.useRef)(false);
4318
+ const applyReadOnly = (0, import_react10.useRef)(async () => {
1553
4319
  });
1554
- (0, import_react7.useEffect)(() => void applyReadOnly.current(turnRunning), [turnRunning]);
1555
- (0, import_react7.useEffect)(() => () => setLocked(false), [setLocked]);
1556
- const [crumbs, setCrumbs] = (0, import_react7.useState)([]);
1557
- const [selectedImageSrc, setSelectedImageSrc] = (0, import_react7.useState)(null);
1558
- const [picker, setPicker] = (0, import_react7.useState)(
4320
+ const [turn] = (0, import_react10.useState)(createTurnFollower);
4321
+ (0, import_react10.useEffect)(() => turn.turn(turnRunning), [turn, turnRunning]);
4322
+ (0, import_react10.useEffect)(() => {
4323
+ turn.mount();
4324
+ return () => {
4325
+ turn.unmount();
4326
+ setLocked(false);
4327
+ };
4328
+ }, [turn, setLocked]);
4329
+ const [crumbs, setCrumbs] = (0, import_react10.useState)([]);
4330
+ const [picker, setPicker] = (0, import_react10.useState)(
1559
4331
  null
1560
4332
  );
1561
4333
  const onEditor = async (editor) => {
@@ -1564,7 +4336,7 @@ function EmailEditor({ docId, onReady, commentsRefresh, onOpenCountChange }) {
1564
4336
  const chain = [];
1565
4337
  let cur = c;
1566
4338
  while (cur) {
1567
- const t = tagOf(cur);
4339
+ const t = tagOf6(cur);
1568
4340
  if (!t || t === "mj-body" || t === "wrapper" || t === "Wrapper") break;
1569
4341
  chain.unshift(cur);
1570
4342
  cur = cur.parent() ?? void 0;
@@ -1613,7 +4385,7 @@ function EmailEditor({ docId, onReady, commentsRefresh, onOpenCountChange }) {
1613
4385
  if (isSection(c)) addIdClass(c, "sec");
1614
4386
  if (!isCommentable(c)) return;
1615
4387
  const toolbar = [...c.get("toolbar") ?? []];
1616
- if (tagOf(c) === "mj-image" && !toolbar.some((t) => t.command === "open-image-picker")) {
4388
+ if (tagOf6(c) === "mj-image" && !toolbar.some((t) => t.command === "open-image-picker")) {
1617
4389
  toolbar.push({
1618
4390
  command: "open-image-picker",
1619
4391
  label: IMAGE_SVG,
@@ -1621,11 +4393,11 @@ function EmailEditor({ docId, onReady, commentsRefresh, onOpenCountChange }) {
1621
4393
  });
1622
4394
  c.set("toolbar", toolbar);
1623
4395
  }
1624
- if (!toolbar.some((t) => t.command === "open-comments")) {
4396
+ if (closestSection(c) && !toolbar.some((t) => t.command === "open-comments")) {
1625
4397
  toolbar.push({
1626
4398
  command: "open-comments",
1627
4399
  label: SPARKLES_SVG,
1628
- attributes: { title: "Comment on element / section", "data-role": "sparkles" }
4400
+ attributes: { title: labels.commentOnElement, "data-role": "sparkles" }
1629
4401
  });
1630
4402
  c.set("toolbar", toolbar);
1631
4403
  }
@@ -1633,7 +4405,7 @@ function EmailEditor({ docId, onReady, commentsRefresh, onOpenCountChange }) {
1633
4405
  editor.Commands.add("open-image-picker", {
1634
4406
  run(ed) {
1635
4407
  const target = ed.getSelected();
1636
- if (!target || tagOf(target) !== "mj-image") return;
4408
+ if (!target || tagOf6(target) !== "mj-image") return;
1637
4409
  const am = ed.AssetManager;
1638
4410
  am.open({
1639
4411
  target,
@@ -1652,21 +4424,29 @@ function EmailEditor({ docId, onReady, commentsRefresh, onOpenCountChange }) {
1652
4424
  }
1653
4425
  });
1654
4426
  setupRichText(editor, labels);
1655
- setupStyleFields(editor, labels);
1656
- const blockCategory = (label) => {
1657
- const l = label.toLowerCase();
1658
- if (/(column|section)/.test(l)) return "Layout";
1659
- if (/(text|image|button|divider|spacer|social)/.test(l)) return "Basic";
1660
- return "Advanced";
4427
+ fixPluginTypes(editor);
4428
+ registerBlocks(editor, blocks, labels.blockCategorySections, SECTION_SVG);
4429
+ setupLayerNames(editor, labels);
4430
+ const STRUCTURE = /* @__PURE__ */ new Set(["mj-wrapper", "mj-section", "mj-column"]);
4431
+ const showBodyInLayers = () => {
4432
+ const body = findByTag("mj-body");
4433
+ if (!body) return;
4434
+ editor.Layers.setRoot(body);
4435
+ body.onAll((c) => {
4436
+ if (STRUCTURE.has(tagOf6(c))) editor.Layers.setOpen(c, true);
4437
+ });
1661
4438
  };
1662
- const bm = editor.BlockManager;
1663
- bm.getAll().forEach(
1664
- (b) => {
1665
- b.set("category", blockCategory(b.get("label") || ""));
4439
+ const keepStructureOpen = () => {
4440
+ const layers = editor.Layers;
4441
+ const opened = layers.model?.get("opened") ?? {};
4442
+ for (const [cid, c] of Object.entries(opened)) {
4443
+ if (STRUCTURE.has(tagOf6(c))) delete opened[cid];
1666
4444
  }
1667
- );
1668
- bm.render();
1669
- editor.on("component:add", decorate);
4445
+ };
4446
+ editor.on("component:add", (c) => {
4447
+ if (!loadingRef.current) freshenCopiedIds(editor.getWrapper(), c);
4448
+ decorate(c);
4449
+ });
1670
4450
  editor.on("load", () => {
1671
4451
  const body = editor.Canvas.getBody();
1672
4452
  body?.addEventListener(
@@ -1676,45 +4456,28 @@ function EmailEditor({ docId, onReady, commentsRefresh, onOpenCountChange }) {
1676
4456
  },
1677
4457
  true
1678
4458
  );
4459
+ editor.Canvas.getDocument()?.addEventListener("keydown", onBlockSearchKey);
4460
+ showBodyInLayers();
1679
4461
  });
1680
4462
  editor.on("component:selected", (c) => {
4463
+ keepStructureOpen();
1681
4464
  if (selectionSource.current === "canvas") setSidebarView("settings");
1682
4465
  selectionSource.current = "other";
1683
4466
  buildCrumbs(c);
1684
- setSelectedImageSrc(c && tagOf(c) === "mj-image" ? String(c.get("src") ?? "") : null);
1685
- if (c?.get("type") === "mj-image" && !c.getTrait("src")) {
1686
- c.addTrait(
1687
- {
1688
- type: "text",
1689
- name: "src",
1690
- label: "Image (URL)",
1691
- placeholder: "https://\u2026",
1692
- changeProp: true
1693
- },
1694
- { at: 0 }
1695
- );
1696
- }
1697
4467
  });
1698
4468
  editor.on("component:deselected", () => {
1699
- const rest = editor.getSelected();
1700
- buildCrumbs(rest);
1701
- setSelectedImageSrc(
1702
- rest && tagOf(rest) === "mj-image" ? String(rest.get("src") ?? "") : null
1703
- );
1704
- });
1705
- editor.on("component:update:src", (c) => {
1706
- if (c === editor.getSelected()) setSelectedImageSrc(String(c.get("src") ?? ""));
4469
+ buildCrumbs(editor.getSelected());
1707
4470
  });
1708
- let stored = "";
4471
+ const baseline = createSaveBaseline();
1709
4472
  const save = async () => {
1710
4473
  const view = editor.getEditing()?.getView();
1711
4474
  await view?.syncContent?.();
1712
4475
  const mjml = editor.getHtml();
1713
- if (mjml === stored) return;
4476
+ if (!baseline.changed(mjml)) return;
1714
4477
  setSave("saving");
1715
4478
  try {
1716
4479
  await documents.save(docId, { mjml, projectData: editor.getProjectData() });
1717
- stored = mjml;
4480
+ baseline.saved(mjml);
1718
4481
  setSave("saved");
1719
4482
  } catch (e) {
1720
4483
  setSave("error");
@@ -1745,6 +4508,7 @@ function EmailEditor({ docId, onReady, commentsRefresh, onOpenCountChange }) {
1745
4508
  });
1746
4509
  }
1747
4510
  const lock = (on) => {
4511
+ if (!turn.mounted) return;
1748
4512
  readOnlyRef.current = on;
1749
4513
  setLocked(on);
1750
4514
  notifyState();
@@ -1770,24 +4534,36 @@ function EmailEditor({ docId, onReady, commentsRefresh, onOpenCountChange }) {
1770
4534
  return;
1771
4535
  }
1772
4536
  lock(false);
4537
+ queue.schedule();
1773
4538
  };
1774
4539
  const reload = () => queue.run(async () => {
1775
4540
  const fresh = await documents.get(docId);
1776
- if (fresh.mjml !== stored) loadMjml(fresh.mjml);
4541
+ if (fresh.mjml !== baseline.stored) loadMjml(fresh.mjml);
1777
4542
  });
1778
4543
  const loadMjml = (mjml) => {
1779
4544
  loadingRef.current = true;
4545
+ let repaired = false;
4546
+ const adds = !mjml || (0, import_core4.listSections)(mjml).some((s) => !s.sectionId);
1780
4547
  try {
1781
4548
  queue.cancel();
1782
- stored = mjml;
1783
- editor.setComponents(mjml || import_core2.STARTER_MJML);
1784
- editor.getWrapper()?.find("mj-section").forEach(decorate);
1785
- for (const t of Object.keys(TYPE_LABEL)) {
1786
- editor.getWrapper()?.find(t).forEach(decorate);
1787
- }
4549
+ baseline.load(mjml, null);
4550
+ const source = sourceForEditor(mjml || import_core4.STARTER_MJML);
4551
+ repaired = source.repaired;
4552
+ editor.setComponents(source.mjml);
4553
+ editor.getWrapper()?.onAll((c) => {
4554
+ const illegal = illegalAttributes(tagOf6(c), c.getAttributes());
4555
+ if (illegal.length === 0) return;
4556
+ c.removeAttributes(illegal);
4557
+ repaired = true;
4558
+ });
4559
+ if (!repaired && !adds) baseline.load(mjml, editor.getHtml());
4560
+ showBodyInLayers();
4561
+ editor.UndoManager.clear();
1788
4562
  } finally {
1789
4563
  loadingRef.current = false;
1790
4564
  }
4565
+ notifyState();
4566
+ if ((repaired || adds) && !readOnlyRef.current) queue.schedule();
1791
4567
  };
1792
4568
  const ensureHighlightStyles = () => {
1793
4569
  const cdoc = editor.Canvas.getDocument();
@@ -1809,7 +4585,10 @@ function EmailEditor({ docId, onReady, commentsRefresh, onOpenCountChange }) {
1809
4585
  cdoc.head.appendChild(style);
1810
4586
  };
1811
4587
  const sectionEl = (sectionId) => {
1812
- const comp = editor.getWrapper()?.find("mj-section").find((c) => sectionIdOf(c) === sectionId);
4588
+ let comp;
4589
+ editor.getWrapper()?.onAll((c) => {
4590
+ if (!comp && isSection(c) && sectionIdOf(c) === sectionId) comp = c;
4591
+ });
1813
4592
  return comp?.getEl?.() ?? void 0;
1814
4593
  };
1815
4594
  const compiledHtml = () => {
@@ -1820,9 +4599,9 @@ function EmailEditor({ docId, onReady, commentsRefresh, onOpenCountChange }) {
1820
4599
  return editor.getHtml();
1821
4600
  }
1822
4601
  };
4602
+ turn.ready((on) => void applyReadOnly.current(on));
1823
4603
  try {
1824
- const doc = await documents.get(docId);
1825
- loadMjml(doc.mjml);
4604
+ await queue.run(async () => loadMjml((await documents.get(docId)).mjml));
1826
4605
  } catch (e) {
1827
4606
  import_sonner2.toast.error(labels.documentLoadFailed);
1828
4607
  console.error(e);
@@ -1874,10 +4653,10 @@ function EmailEditor({ docId, onReady, commentsRefresh, onOpenCountChange }) {
1874
4653
  }
1875
4654
  });
1876
4655
  };
1877
- return /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
1878
- import_react6.default,
4656
+ return /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
4657
+ import_react9.default,
1879
4658
  {
1880
- grapesjs: import_grapesjs.default,
4659
+ grapesjs: import_grapesjs2.default,
1881
4660
  className: "h-full",
1882
4661
  options: {
1883
4662
  height: "100%",
@@ -1886,12 +4665,13 @@ function EmailEditor({ docId, onReady, commentsRefresh, onOpenCountChange }) {
1886
4665
  // app.grapesjs.com and leave a session-storage entry, which a host would then have
1887
4666
  // to disclose and, for the entry, ask consent for.
1888
4667
  telemetry: false,
4668
+ canvasCss: CANVAS_CSS,
1889
4669
  fromElement: false,
1890
4670
  panels: { defaults: [] },
1891
4671
  blockManager: { appendTo: "#gjs-blocks" },
1892
- styleManager: { appendTo: "#gjs-styles" },
1893
- traitManager: { appendTo: "#gjs-traits" },
1894
- layerManager: { appendTo: "#gjs-layers" },
4672
+ // The eye hides a layer by writing `display="none"` into the MJML, which is illegal.
4673
+ // The root row is mj-body, which the sidebar reaches as "Email settings" instead.
4674
+ layerManager: { appendTo: "#gjs-layers", hidable: false, showWrapper: false },
1895
4675
  assetManager: {
1896
4676
  // Never inline files: mail clients block `data:` images (see ImageLibrary).
1897
4677
  embedAsBase64: false,
@@ -1905,115 +4685,118 @@ function EmailEditor({ docId, onReady, commentsRefresh, onOpenCountChange }) {
1905
4685
  close: () => setPicker(null)
1906
4686
  }
1907
4687
  },
4688
+ canvas: {
4689
+ customBadgeLabel: badgeLabel(labels),
4690
+ // A file or text dragged in from outside lands as `<img>` or `<div>`, which no MJML
4691
+ // validator accepts; drags from the palette are unaffected.
4692
+ allowExternalDrop: false
4693
+ },
1908
4694
  deviceManager: {
1909
4695
  devices: [
1910
4696
  { id: "desktop", name: "Desktop", width: "" },
1911
4697
  { id: "mobile", name: "Mobile", width: "375px", widthMedia: "480px" }
1912
4698
  ]
1913
4699
  },
1914
- plugins: [import_grapesjs_mjml.default]
4700
+ plugins: [
4701
+ (editor) => (0, import_grapesjs_mjml2.default)(editor, {
4702
+ blocks: BUILTIN_BLOCKS,
4703
+ block: builtinBlock(labels)
4704
+ })
4705
+ ]
1915
4706
  },
1916
4707
  onEditor,
1917
- children: /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("div", { className: "relative flex h-full min-h-0 flex-1", inert: readOnly, children: [
1918
- readOnly && /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("div", { className: "absolute inset-0 z-30 flex cursor-not-allowed items-start justify-center bg-white/20 pt-3", children: /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("span", { className: "flex items-center gap-2 rounded-full bg-panel px-3 py-1 text-xs text-panel-fg shadow", children: [
1919
- /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(Spinner, {}),
1920
- " ",
1921
- labels.agentWorking
1922
- ] }) }),
1923
- /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
1924
- LeftSidebar,
1925
- {
1926
- view: sidebarView,
1927
- onViewChange: setSidebarView,
1928
- markLayersSource: () => selectionSource.current = "layers",
1929
- selectedImageSrc,
1930
- onChooseImage: () => editorRef.current?.runCommand("open-image-picker")
1931
- }
1932
- ),
1933
- /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("div", { className: "flex min-w-0 flex-1 flex-col bg-surface-muted", children: [
1934
- /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("div", { className: "relative min-h-0 flex-1", children: [
1935
- /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(import_react6.Canvas, { className: "h-full" }),
1936
- /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
1937
- CanvasComments,
1938
- {
1939
- docId,
1940
- composeTarget,
1941
- onComposeConsumed: () => setComposeTarget(null),
1942
- refreshSignal: commentsRefresh,
1943
- onOpenCountChange
1944
- }
1945
- ),
1946
- /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
1947
- ImagePicker,
4708
+ children: /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)(
4709
+ "div",
4710
+ {
4711
+ className: "relative flex h-full min-h-0 flex-1",
4712
+ inert: readOnly,
4713
+ onKeyDown: (e) => onBlockSearchKey(e.nativeEvent),
4714
+ children: [
4715
+ readOnly && /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("div", { className: "absolute inset-0 z-30 flex cursor-not-allowed items-start justify-center bg-white/20 pt-3", children: /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("span", { className: "flex items-center gap-2 rounded-full bg-panel px-3 py-1 text-xs text-panel-fg shadow", children: [
4716
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(Spinner, {}),
4717
+ " ",
4718
+ labels.agentWorking
4719
+ ] }) }),
4720
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
4721
+ LeftSidebar,
1948
4722
  {
1949
- open: picker !== null,
1950
- currentUrl: picker?.current,
1951
- onSelect: (url) => {
1952
- if (!readOnlyRef.current) picker?.select(url);
1953
- setPicker(null);
1954
- },
1955
- onClose: () => {
1956
- setPicker(null);
1957
- editorRef.current?.AssetManager.close();
1958
- }
4723
+ view: sidebarView,
4724
+ onViewChange: setSidebarView,
4725
+ markLayersSource: () => selectionSource.current = "layers",
4726
+ blockSearchRef
1959
4727
  }
1960
4728
  ),
1961
- loading && /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("div", { className: "absolute inset-0 z-10 flex items-center justify-center gap-2 bg-white/70 text-sm text-zinc-500", children: [
1962
- /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(Spinner, {}),
1963
- " Loading editor\u2026"
4729
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("div", { className: "flex min-w-0 flex-1 flex-col bg-surface-muted", children: [
4730
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("div", { className: "relative min-h-0 flex-1", children: [
4731
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_react9.Canvas, { className: "h-full" }),
4732
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
4733
+ CanvasComments,
4734
+ {
4735
+ docId,
4736
+ composeTarget,
4737
+ onComposeConsumed: () => setComposeTarget(null),
4738
+ refreshSignal: commentsRefresh,
4739
+ onOpenCountChange
4740
+ }
4741
+ ),
4742
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
4743
+ ImagePicker,
4744
+ {
4745
+ open: picker !== null,
4746
+ currentUrl: picker?.current,
4747
+ onSelect: (url) => {
4748
+ if (!readOnlyRef.current) picker?.select(url);
4749
+ setPicker(null);
4750
+ },
4751
+ onClose: () => {
4752
+ setPicker(null);
4753
+ editorRef.current?.AssetManager.close();
4754
+ }
4755
+ }
4756
+ ),
4757
+ loading && /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("div", { className: "absolute inset-0 z-10 flex items-center justify-center gap-2 bg-white/70 text-sm text-zinc-500", children: [
4758
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(Spinner, {}),
4759
+ " ",
4760
+ labels.loadingEditor
4761
+ ] })
4762
+ ] }),
4763
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(Breadcrumb, { crumbs, onSelect: (c) => editorRef.current?.select(c) })
1964
4764
  ] })
1965
- ] }),
1966
- /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(Breadcrumb, { crumbs, onSelect: (c) => editorRef.current?.select(c) })
1967
- ] })
1968
- ] })
4765
+ ]
4766
+ }
4767
+ )
1969
4768
  }
1970
4769
  );
1971
4770
  }
1972
- function CollapseSection({
1973
- title,
1974
- defaultOpen = true,
1975
- children
1976
- }) {
1977
- const [open, setOpen] = (0, import_react7.useState)(defaultOpen);
1978
- return /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("div", { className: "border-b border-panel-border", children: [
1979
- /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)(
1980
- "button",
1981
- {
1982
- type: "button",
1983
- onClick: () => setOpen((o) => !o),
1984
- className: "flex w-full items-center gap-1.5 px-3 py-2 text-xs font-semibold tracking-wide text-panel-muted-fg uppercase hover:text-panel-fg",
1985
- children: [
1986
- /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(import_lucide_react7.ChevronRight, { className: cn("size-3.5 transition-transform", open && "rotate-90") }),
1987
- title
1988
- ]
1989
- }
1990
- ),
1991
- /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("div", { className: cn(!open && "hidden"), children })
1992
- ] });
1993
- }
1994
4771
  function LeftSidebar({
1995
4772
  view,
1996
4773
  onViewChange,
1997
4774
  markLayersSource,
1998
- selectedImageSrc,
1999
- onChooseImage
4775
+ blockSearchRef
2000
4776
  }) {
2001
- const editor = (0, import_react6.useEditorMaybe)();
4777
+ const editor = (0, import_react9.useEditorMaybe)();
2002
4778
  const labels = useLabels();
2003
- const [layersExpanded, setLayersExpanded] = (0, import_react7.useState)(false);
2004
- const [blockSearch, setBlockSearch] = (0, import_react7.useState)("");
2005
- const changeView = (v) => {
2006
- if (v === "settings" && editor && !editor.getSelected()) {
2007
- const wrapper = editor.getWrapper();
2008
- const body = wrapper?.find("mj-body")[0] ?? wrapper;
2009
- if (body) editor.select(body);
2010
- }
2011
- onViewChange(v);
4779
+ const [layersExpanded, setLayersExpanded] = (0, import_react10.useState)(false);
4780
+ const [blockSearch, setBlockSearch] = (0, import_react10.useState)("");
4781
+ const [noBlockMatches, setNoBlockMatches] = (0, import_react10.useState)(false);
4782
+ const selectBody = () => {
4783
+ let body;
4784
+ editor?.getWrapper()?.onAll((c) => {
4785
+ if (!body && tagOf6(c) === "mj-body") body = c;
4786
+ });
4787
+ if (body) editor?.select(body);
2012
4788
  };
2013
4789
  const toggleLayers = (expand) => {
2014
4790
  setLayersExpanded(expand);
2015
4791
  editor?.getWrapper()?.onAll((c) => c.set("open", expand));
2016
4792
  };
4793
+ const onTileKey = (e) => {
4794
+ const tile = e.target.closest("[data-block]");
4795
+ const block = tile && editor?.Blocks.get(tile.dataset.block ?? "");
4796
+ if (!editor || !block || !addsBlock(e)) return;
4797
+ e.preventDefault();
4798
+ if (insertBlock(editor, block)) (0, import_sonner2.toast)(labels.blockAdded(tile.textContent?.trim() ?? ""));
4799
+ };
2017
4800
  const filterBlocks = (q) => {
2018
4801
  setBlockSearch(q);
2019
4802
  const root = document.getElementById("gjs-blocks");
@@ -2029,17 +4812,22 @@ function LeftSidebar({
2029
4812
  );
2030
4813
  cat.style.display = anyVisible ? "" : "none";
2031
4814
  });
4815
+ setNoBlockMatches(
4816
+ [...root.querySelectorAll(".gjs-block")].every(
4817
+ (b) => b.style.display === "none"
4818
+ )
4819
+ );
2032
4820
  };
2033
- return /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("div", { className: "editor-dark flex w-64 shrink-0 flex-col border-r border-panel-border bg-panel text-panel-fg", children: [
2034
- /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("div", { className: "flex border-b border-panel-border", children: [
2035
- ["blocks", "Blocks"],
2036
- ["settings", "Settings"],
2037
- ["layers", "Layers"]
2038
- ].map(([value, label]) => /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
4821
+ return /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("div", { className: "editor-dark flex w-64 shrink-0 flex-col border-r border-panel-border bg-panel text-panel-fg", children: [
4822
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("div", { className: "flex border-b border-panel-border", children: [
4823
+ ["blocks", labels.tabBlocks],
4824
+ ["settings", labels.tabSettings],
4825
+ ["layers", labels.tabLayers]
4826
+ ].map(([value, label]) => /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
2039
4827
  "button",
2040
4828
  {
2041
4829
  type: "button",
2042
- onClick: () => changeView(value),
4830
+ onClick: () => onViewChange(value),
2043
4831
  className: cn(
2044
4832
  "relative flex-1 px-3 py-2.5 text-sm transition-colors",
2045
4833
  "after:absolute after:inset-x-0 after:-bottom-px after:h-0.5 after:transition-colors",
@@ -2049,70 +4837,57 @@ function LeftSidebar({
2049
4837
  },
2050
4838
  value
2051
4839
  )) }),
2052
- /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("div", { className: cn("flex min-h-0 flex-1 flex-col", view !== "blocks" && "hidden"), children: [
2053
- /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("div", { className: "relative border-b border-panel-border p-2", children: [
2054
- /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(import_lucide_react8.Search, { className: "pointer-events-none absolute top-1/2 left-4 size-3.5 -translate-y-1/2 text-panel-muted-fg" }),
2055
- /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
4840
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("div", { className: cn("flex min-h-0 flex-1 flex-col", view !== "blocks" && "hidden"), children: [
4841
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("div", { className: "relative border-b border-panel-border p-2", children: [
4842
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_lucide_react11.Search, { className: "pointer-events-none absolute top-1/2 left-4 size-3.5 -translate-y-1/2 text-panel-muted-fg" }),
4843
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
2056
4844
  Input,
2057
4845
  {
4846
+ ref: blockSearchRef,
2058
4847
  value: blockSearch,
2059
4848
  onChange: (e) => filterBlocks(e.target.value),
2060
4849
  placeholder: labels.searchBlocks,
2061
4850
  className: "h-8 border-panel-border bg-panel-elevated pr-10 pl-7 text-sm text-panel-fg placeholder:text-panel-muted-fg"
2062
4851
  }
2063
4852
  ),
2064
- /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("kbd", { className: "pointer-events-none absolute top-1/2 right-4 -translate-y-1/2 rounded border border-panel-border bg-panel px-1 py-0.5 font-mono text-[10px] text-panel-muted-fg", children: "\u2318K" })
2065
- ] }),
2066
- /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("div", { id: "gjs-blocks", className: "min-h-0 flex-1 overflow-y-auto" })
2067
- ] }),
2068
- /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("div", { className: cn("min-h-0 flex-1 overflow-y-auto", view !== "settings" && "hidden"), children: [
2069
- selectedImageSrc !== null && /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("div", { className: "border-b border-panel-border p-3", children: [
2070
- /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)(
2071
- "button",
2072
- {
2073
- type: "button",
2074
- onClick: onChooseImage,
2075
- title: labels.chooseImage,
2076
- className: "group relative block w-full overflow-hidden rounded-lg border border-panel-border bg-panel-elevated",
2077
- children: [
2078
- selectedImageSrc ? /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
2079
- "img",
2080
- {
2081
- src: selectedImageSrc,
2082
- alt: "",
2083
- className: "h-32 w-full bg-black/25 object-contain"
2084
- }
2085
- ) : /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("span", { className: "flex h-32 items-center justify-center text-panel-muted-fg", children: /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(import_lucide_react7.ImageIcon, { className: "size-8" }) }),
2086
- /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("span", { className: "absolute inset-0 flex items-center justify-center bg-black/50 text-sm font-medium text-white opacity-0 transition group-hover:opacity-100", children: labels.chooseImage })
2087
- ]
2088
- }
2089
- ),
2090
- /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)(
2091
- Button,
2092
- {
2093
- type: "button",
2094
- size: "sm",
2095
- className: "mt-2 w-full bg-brand text-brand-fg hover:bg-brand/90",
2096
- onClick: onChooseImage,
2097
- children: [
2098
- /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(import_lucide_react7.ImageUp, {}),
2099
- labels.chooseImage
2100
- ]
2101
- }
2102
- )
4853
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("kbd", { className: "pointer-events-none absolute top-1/2 right-4 -translate-y-1/2 rounded border border-panel-border bg-panel px-1 py-0.5 font-mono text-[10px] text-panel-muted-fg", children: "/" })
2103
4854
  ] }),
2104
- /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(CollapseSection, { title: labels.attributes, children: /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("div", { id: "gjs-traits" }) }),
2105
- /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(CollapseSection, { title: labels.style, children: /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("div", { id: "gjs-styles" }) })
4855
+ noBlockMatches && /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("p", { className: "px-3 py-6 text-center text-xs text-panel-muted-fg", children: labels.noBlocksMatch }),
4856
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("div", { id: "gjs-blocks", className: "min-h-0 flex-1 overflow-y-auto", onKeyDown: onTileKey })
2106
4857
  ] }),
2107
- /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)(
4858
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("div", { className: cn("min-h-0 flex-1 overflow-y-auto", view !== "settings" && "hidden"), children: /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
4859
+ Inspector,
4860
+ {
4861
+ onEmailSettings: selectBody,
4862
+ empty: /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("div", { className: "flex flex-col items-center gap-3 px-6 py-10 text-center", children: [
4863
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_lucide_react10.MousePointerClick, { className: "size-6 text-panel-muted-fg" }),
4864
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("p", { className: "text-sm text-panel-muted-fg", children: labels.settingsEmpty }),
4865
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)(
4866
+ Button,
4867
+ {
4868
+ type: "button",
4869
+ size: "sm",
4870
+ variant: "outline",
4871
+ className: "border-panel-border bg-panel-elevated text-panel-fg hover:bg-panel-hover hover:text-panel-fg",
4872
+ onClick: selectBody,
4873
+ children: [
4874
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_lucide_react10.Mail, {}),
4875
+ labels.emailSettings
4876
+ ]
4877
+ }
4878
+ )
4879
+ ] })
4880
+ }
4881
+ ) }),
4882
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)(
2108
4883
  "div",
2109
4884
  {
2110
4885
  className: cn("flex min-h-0 flex-1 flex-col", view !== "layers" && "hidden"),
2111
4886
  onMouseDownCapture: markLayersSource,
2112
4887
  children: [
2113
- /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("label", { className: "flex items-center justify-between gap-2 border-b border-panel-border px-3 py-2 text-xs text-panel-muted-fg", children: [
2114
- /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("span", { children: labels.expandAll }),
2115
- /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
4888
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("label", { className: "flex items-center justify-between gap-2 border-b border-panel-border px-3 py-2 text-xs text-panel-muted-fg", children: [
4889
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("span", { children: labels.expandAll }),
4890
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
2116
4891
  Switch,
2117
4892
  {
2118
4893
  checked: layersExpanded,
@@ -2121,7 +4896,16 @@ function LeftSidebar({
2121
4896
  }
2122
4897
  )
2123
4898
  ] }),
2124
- /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("div", { id: "gjs-layers", className: "min-h-0 flex-1 overflow-y-auto" })
4899
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
4900
+ "div",
4901
+ {
4902
+ id: "gjs-layers",
4903
+ className: "min-h-0 flex-1 overflow-y-auto",
4904
+ onDoubleClickCapture: (e) => {
4905
+ if (e.target.closest("[data-name]")) e.stopPropagation();
4906
+ }
4907
+ }
4908
+ )
2125
4909
  ]
2126
4910
  }
2127
4911
  )
@@ -2132,9 +4916,9 @@ function Breadcrumb({
2132
4916
  onSelect
2133
4917
  }) {
2134
4918
  const labels = useLabels();
2135
- return /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("div", { className: "flex h-9 shrink-0 items-center gap-0.5 border-t border-border bg-surface px-3 text-xs text-muted-foreground", children: crumbs.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("span", { className: "text-muted-foreground/60", children: labels.selectAnElement }) : crumbs.map((c, i) => /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("span", { className: "flex items-center gap-0.5", children: [
2136
- i > 0 && /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(import_lucide_react7.ChevronRight, { className: "size-3 opacity-40" }),
2137
- /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
4919
+ return /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("div", { className: "flex h-9 shrink-0 items-center gap-0.5 border-t border-border bg-surface px-3 text-xs text-muted-foreground", children: crumbs.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("span", { className: "text-muted-foreground/60", children: labels.selectAnElement }) : crumbs.map((c, i) => /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("span", { className: "flex items-center gap-0.5", children: [
4920
+ i > 0 && /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_lucide_react10.ChevronRight, { className: "size-3 opacity-40" }),
4921
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
2138
4922
  "button",
2139
4923
  {
2140
4924
  type: "button",
@@ -2143,13 +4927,14 @@ function Breadcrumb({
2143
4927
  "rounded px-1.5 py-0.5 hover:bg-muted hover:text-foreground",
2144
4928
  i === crumbs.length - 1 && "font-medium text-brand"
2145
4929
  ),
2146
- children: crumbName(c)
4930
+ children: crumbName(c, labels)
2147
4931
  }
2148
4932
  )
2149
4933
  ] }, i)) });
2150
4934
  }
2151
4935
  // Annotate the CommonJS export names for ESM import in node:
2152
4936
  0 && (module.exports = {
2153
- EmailEditor
4937
+ EmailEditor,
4938
+ blockProblems
2154
4939
  });
2155
4940
  //# sourceMappingURL=canvas.cjs.map