@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.js CHANGED
@@ -16,20 +16,48 @@ import {
16
16
  useDocumentStore,
17
17
  useImageLibrary,
18
18
  useLabels
19
- } from "./chunk-VJRKDFM4.js";
19
+ } from "./chunk-K7KQC7LX.js";
20
20
  import {
21
21
  Button,
22
22
  Input,
23
23
  Spinner,
24
+ Tooltip,
25
+ TooltipContent,
26
+ TooltipProvider,
27
+ TooltipTrigger,
24
28
  cn
25
29
  } from "./chunk-V6RR4VRD.js";
26
30
 
27
31
  // src/components/editor/EmailEditor.tsx
28
- import grapesjs from "grapesjs";
29
- import GjsEditor, { Canvas, useEditorMaybe } from "@grapesjs/react";
30
- import grapesjsMJML from "grapesjs-mjml";
31
- import { useEffect as useEffect3, useRef as useRef3, useState as useState3 } from "react";
32
- import { STARTER_MJML } from "@mjml-agent-editor/core";
32
+ import grapesjs2 from "grapesjs";
33
+ import GjsEditor, { Canvas, useEditorMaybe as useEditorMaybe2 } from "@grapesjs/react";
34
+ import grapesjsMJML2 from "grapesjs-mjml";
35
+ import { useEffect as useEffect5, useRef as useRef5, useState as useState5 } from "react";
36
+ import { listSections, STARTER_MJML } from "@mjml-agent-editor/core";
37
+
38
+ // src/lib/save-baseline.ts
39
+ function createSaveBaseline() {
40
+ let stored = "";
41
+ let loaded = null;
42
+ return {
43
+ /** The MJML the store holds, as far as this editor knows. */
44
+ get stored() {
45
+ return stored;
46
+ },
47
+ /** `canvas` is what the load serialises to, or null when it repaired something to write. */
48
+ load(mjml, canvas) {
49
+ stored = mjml;
50
+ loaded = canvas;
51
+ },
52
+ saved(mjml) {
53
+ stored = mjml;
54
+ loaded = null;
55
+ },
56
+ changed(mjml) {
57
+ return mjml !== stored && mjml !== loaded;
58
+ }
59
+ };
60
+ }
33
61
 
34
62
  // src/lib/save-queue.ts
35
63
  function createSaveQueue(save, delayMs) {
@@ -60,9 +88,50 @@ function createSaveQueue(save, delayMs) {
60
88
  };
61
89
  }
62
90
 
91
+ // src/lib/turn-follower.ts
92
+ function createTurnFollower() {
93
+ let running = false;
94
+ let mounted = true;
95
+ let apply = null;
96
+ return {
97
+ turn(on) {
98
+ if (on === running) return;
99
+ running = on;
100
+ if (mounted) apply?.(on);
101
+ },
102
+ /**
103
+ * Whether the canvas is still mounted. An unmounted one's late reload must not unlock
104
+ * the shared state a canvas mounted since has locked.
105
+ */
106
+ get mounted() {
107
+ return mounted;
108
+ },
109
+ // Both, as React mounts twice in development.
110
+ mount() {
111
+ mounted = true;
112
+ },
113
+ unmount() {
114
+ mounted = false;
115
+ },
116
+ /** A turn already under way is applied at once; none means nothing to apply. */
117
+ ready(fn) {
118
+ apply = fn;
119
+ if (running) fn(true);
120
+ }
121
+ };
122
+ }
123
+
124
+ // src/lib/shortcuts.ts
125
+ function opensBlockSearch(e) {
126
+ if (e.key !== "/" || e.metaKey || e.ctrlKey || e.altKey || e.isComposing) return false;
127
+ const target = e.target;
128
+ if (target?.isContentEditable) return false;
129
+ return !["INPUT", "TEXTAREA", "SELECT", "IFRAME"].includes(target?.tagName ?? "");
130
+ }
131
+
63
132
  // src/components/editor/EmailEditor.tsx
64
133
  import { toast as toast2 } from "sonner";
65
- import { ChevronRight, ImageIcon as ImageIcon2, ImageUp as ImageUp2 } from "lucide-react";
134
+ import { ChevronRight as ChevronRight3, Mail, MousePointerClick } from "lucide-react";
66
135
 
67
136
  // src/components/ui/switch.tsx
68
137
  import "react";
@@ -353,11 +422,1051 @@ function matchesAccept(file, accept) {
353
422
  });
354
423
  }
355
424
 
425
+ // src/components/editor/inspector/Inspector.tsx
426
+ import { useEffect as useEffect4, useLayoutEffect, useReducer, useRef as useRef4, useState as useState4 } from "react";
427
+ import { useEditorMaybe } from "@grapesjs/react";
428
+ import { ArrowUp, ChevronRight as ChevronRight2, Copy, Monitor, Trash2 as Trash22, X as X2 } from "lucide-react";
429
+
356
430
  // src/components/editor/ColorField.tsx
357
- import { useCallback as useCallback2, useEffect as useEffect2, useRef as useRef2, useState as useState2 } from "react";
431
+ import { useCallback as useCallback2, useEffect as useEffect3, useRef as useRef3, useState as useState3 } from "react";
358
432
  import { HexAlphaColorPicker, HexColorPicker } from "react-colorful";
359
433
  import { Pipette } from "lucide-react";
434
+
435
+ // src/components/editor/inspector/controls.tsx
436
+ import {
437
+ createContext,
438
+ useContext,
439
+ useEffect as useEffect2,
440
+ useId,
441
+ useRef as useRef2,
442
+ useState as useState2
443
+ } from "react";
444
+ import {
445
+ AlignCenter,
446
+ AlignJustify,
447
+ AlignLeft,
448
+ AlignRight,
449
+ ChevronDown,
450
+ ChevronUp,
451
+ CircleHelp,
452
+ Grid2x2,
453
+ RotateCcw
454
+ } from "lucide-react";
455
+
456
+ // src/components/editor/inspector/values.ts
457
+ import { isSafeLink, isSafeNavbarLink } from "@mjml-agent-editor/core";
458
+ var SIDES = ["top", "right", "bottom", "left"];
459
+ function expandBox(value) {
460
+ const parts = value.trim().split(/\s+/).filter(Boolean);
461
+ if (parts.length === 0 || parts.length > 4) return null;
462
+ const [t, r = t, b = t, l = r] = parts;
463
+ return [t, r, b, l];
464
+ }
465
+ function readBox(attributes, attr) {
466
+ const short = expandBox(attributes[attr] ?? "") ?? ["", "", "", ""];
467
+ return SIDES.map((side, i) => attributes[`${attr}-${side}`] ?? short[i]);
468
+ }
469
+ function boxShorthand(sides) {
470
+ const [t, r, b, l] = sides.map((s) => s.trim() || "0");
471
+ if (t === b && r === l) return t === r ? t : `${t} ${r}`;
472
+ if (r === l) return `${t} ${r} ${b}`;
473
+ return `${t} ${r} ${b} ${l}`;
474
+ }
475
+ function parseLength(value, units) {
476
+ const match = /^(-?\d*\.?\d+)\s*(px|%|em)?$/.exec(value.trim());
477
+ if (!match) return null;
478
+ const unit = match[2] ?? "";
479
+ return units.includes(unit) ? { amount: match[1], unit } : null;
480
+ }
481
+ var NO_SHARE = "-noshare";
482
+ function parseNetwork(name) {
483
+ return name.endsWith(NO_SHARE) ? { network: name.slice(0, -NO_SHARE.length), share: false } : { network: name, share: name !== "custom" && name !== "" };
484
+ }
485
+ function networkName(network, share) {
486
+ return share || network === "custom" ? network : `${network}${NO_SHARE}`;
487
+ }
488
+
489
+ // src/components/editor/inspector/controls.tsx
360
490
  import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
491
+ 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";
492
+ var FieldLabel = createContext({});
493
+ var useFieldLabel = () => {
494
+ const { label, help } = useContext(FieldLabel);
495
+ return { "aria-labelledby": label, "aria-describedby": help };
496
+ };
497
+ function Row({
498
+ label,
499
+ help,
500
+ own,
501
+ wide,
502
+ inline,
503
+ labels,
504
+ onReset,
505
+ children
506
+ }) {
507
+ const id = useId();
508
+ const helpId = useId();
509
+ const [helpOpen, setHelpOpen] = useState2(false);
510
+ const control = /* @__PURE__ */ jsx3(FieldLabel.Provider, { value: { label: id, help: help ? helpId : void 0 }, children });
511
+ const caption = /* @__PURE__ */ jsxs2("span", { className: "flex min-w-0 items-center gap-1.5 text-[11px] text-panel-muted-fg", children: [
512
+ /* @__PURE__ */ jsx3(
513
+ "span",
514
+ {
515
+ "aria-hidden": true,
516
+ className: cn("size-1.5 shrink-0 rounded-full", own ? "bg-brand" : "bg-transparent")
517
+ }
518
+ ),
519
+ /* @__PURE__ */ jsx3("span", { id, className: "truncate", children: label }),
520
+ help && // Its own provider: a host need not wrap the editor in one, as only EditorHeader asked.
521
+ // Opened by a tap as well: Radix opens a tooltip on hover and focus, not on touch.
522
+ /* @__PURE__ */ jsx3(TooltipProvider, { children: /* @__PURE__ */ jsxs2(Tooltip, { open: helpOpen, onOpenChange: setHelpOpen, children: [
523
+ /* @__PURE__ */ jsx3(TooltipTrigger, { asChild: true, children: /* @__PURE__ */ jsx3(
524
+ "button",
525
+ {
526
+ type: "button",
527
+ "aria-label": help,
528
+ onClick: () => setHelpOpen((open) => !open),
529
+ className: "-m-0.5 shrink-0 rounded p-0.5 text-panel-muted-fg/70 hover:text-panel-fg focus-visible:text-panel-fg",
530
+ children: /* @__PURE__ */ jsx3(CircleHelp, { className: "size-3" })
531
+ }
532
+ ) }),
533
+ /* @__PURE__ */ jsx3(TooltipContent, { side: "top", className: "max-w-56 leading-snug", children: help })
534
+ ] }) }),
535
+ own && /* @__PURE__ */ jsx3(
536
+ "button",
537
+ {
538
+ type: "button",
539
+ onClick: onReset,
540
+ title: labels.resetToDefault,
541
+ "aria-label": `${labels.resetToDefault}: ${label}`,
542
+ 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",
543
+ children: /* @__PURE__ */ jsx3(RotateCcw, { className: "size-3" })
544
+ }
545
+ )
546
+ ] });
547
+ return /* @__PURE__ */ jsxs2("div", { className: "group/row px-3 py-1", children: [
548
+ wide ? /* @__PURE__ */ jsxs2("div", { className: "flex flex-col gap-1", children: [
549
+ caption,
550
+ control
551
+ ] }) : inline ? /* @__PURE__ */ jsxs2("div", { className: "flex items-center justify-between gap-2", children: [
552
+ caption,
553
+ control
554
+ ] }) : /* @__PURE__ */ jsxs2("div", { className: "grid grid-cols-[92px_minmax(0,1fr)] items-center gap-2", children: [
555
+ caption,
556
+ control
557
+ ] }),
558
+ help && /* @__PURE__ */ jsx3("span", { id: helpId, hidden: true, children: help })
559
+ ] });
560
+ }
561
+ function useDraft(value, commit, source = value) {
562
+ const [draft, setDraft] = useState2(value);
563
+ useEffect2(() => setDraft(value), [value, source]);
564
+ const done = () => {
565
+ if (draft.trim() !== value) commit(draft.trim());
566
+ };
567
+ const onKeyDown = (e) => {
568
+ if (e.key === "Enter") e.currentTarget.blur();
569
+ if (e.key === "Escape") {
570
+ setDraft(value);
571
+ requestAnimationFrame(() => e.target.blur());
572
+ }
573
+ };
574
+ return { draft, setDraft, done, onKeyDown };
575
+ }
576
+ function TextInput({
577
+ value,
578
+ placeholder,
579
+ accept,
580
+ onCommit
581
+ }) {
582
+ const [invalid, setInvalid] = useState2(false);
583
+ const field = useFieldLabel();
584
+ const { draft, setDraft, done, onKeyDown } = useDraft(value, (next) => {
585
+ const ok = !accept || accept(next);
586
+ setInvalid(!ok);
587
+ if (ok) onCommit(next);
588
+ });
589
+ return /* @__PURE__ */ jsx3(
590
+ "input",
591
+ {
592
+ className: cn(FIELD, invalid && "border-red-400"),
593
+ ...field,
594
+ "aria-invalid": invalid,
595
+ value: draft,
596
+ placeholder,
597
+ onChange: (e) => setDraft(e.target.value),
598
+ onBlur: done,
599
+ onKeyDown
600
+ }
601
+ );
602
+ }
603
+ var BARE_NUMBER = /^-?\d*\.?\d+$/;
604
+ function normaliseLength(text, units, negative = false, unit = units[0]) {
605
+ if (text === "") return "";
606
+ const parsed = BARE_NUMBER.test(text) ? { amount: text, unit } : parseLength(text, units);
607
+ if (!parsed || !negative && parsed.amount.startsWith("-")) return null;
608
+ return `${parsed.amount}${parsed.unit}`;
609
+ }
610
+ function stepAmount(amount, delta, negative = false) {
611
+ const moved = Math.round((Number(amount) + delta) * 100) / 100;
612
+ const next = String(negative ? moved : Math.max(0, moved));
613
+ return BARE_NUMBER.test(next) ? next : amount;
614
+ }
615
+ var REPEAT_AFTER_MS = 400;
616
+ var REPEAT_EVERY_MS = 60;
617
+ function LengthInput({
618
+ value,
619
+ placeholder,
620
+ units,
621
+ negative = false,
622
+ compact = false,
623
+ labelledBy,
624
+ labels,
625
+ onCommit
626
+ }) {
627
+ const fieldLabel = useFieldLabel();
628
+ const own = parseLength(value, units);
629
+ const fallback = parseLength(placeholder ?? "", units);
630
+ const [picked, setPicked] = useState2(null);
631
+ const unit = own?.unit ?? picked ?? fallback?.unit ?? units[0];
632
+ const [invalid, setInvalid] = useState2(false);
633
+ const commit = useRef2(onCommit);
634
+ commit.current = onCommit;
635
+ const endHold = useRef2(null);
636
+ useEffect2(() => () => endHold.current?.(), []);
637
+ const { draft, setDraft, done, onKeyDown } = useDraft(
638
+ own ? own.amount : value,
639
+ (next) => {
640
+ endHold.current?.();
641
+ const normal = normaliseLength(next, units, negative, unit);
642
+ setInvalid(normal === null);
643
+ if (normal !== null) commit.current(normal);
644
+ },
645
+ value
646
+ );
647
+ const from = () => {
648
+ const text = draft.trim();
649
+ if (BARE_NUMBER.test(text)) return { amount: text, unit };
650
+ if (text === "") return { amount: fallback?.amount ?? "0", unit };
651
+ return parseLength(text, units);
652
+ };
653
+ const stepOnce = (delta) => {
654
+ endHold.current?.();
655
+ const start = from();
656
+ if (!start) return setInvalid(true);
657
+ const amount = stepAmount(start.amount, delta, negative);
658
+ setInvalid(false);
659
+ setDraft(amount);
660
+ commit.current(`${amount}${start.unit}`);
661
+ };
662
+ const onKey = (e) => {
663
+ if (e.key !== "ArrowUp" && e.key !== "ArrowDown") return onKeyDown(e);
664
+ e.preventDefault();
665
+ stepOnce((e.key === "ArrowUp" ? 1 : -1) * (e.shiftKey ? 10 : 1));
666
+ };
667
+ const press = (direction) => (e) => {
668
+ if (e.button !== 0 || endHold.current) return;
669
+ e.preventDefault();
670
+ const start = from();
671
+ if (!start) return setInvalid(true);
672
+ const delta = direction * (e.shiftKey ? 10 : 1);
673
+ let amount = stepAmount(start.amount, delta, negative);
674
+ const show = () => {
675
+ setInvalid(false);
676
+ setDraft(amount);
677
+ commit.current(`${amount}${start.unit}`, true);
678
+ };
679
+ show();
680
+ let timer = window.setTimeout(function repeat() {
681
+ amount = stepAmount(amount, delta, negative);
682
+ show();
683
+ timer = window.setTimeout(repeat, REPEAT_EVERY_MS);
684
+ }, REPEAT_AFTER_MS);
685
+ const button = e.currentTarget;
686
+ try {
687
+ button.setPointerCapture(e.pointerId);
688
+ } catch {
689
+ }
690
+ const events = ["pointerup", "pointercancel", "lostpointercapture"];
691
+ const pointerId = e.pointerId;
692
+ const end = () => {
693
+ window.clearTimeout(timer);
694
+ for (const type of events) button.removeEventListener(type, release);
695
+ window.removeEventListener("pointerup", release);
696
+ window.removeEventListener("blur", end);
697
+ endHold.current = null;
698
+ commit.current(`${amount}${start.unit}`);
699
+ };
700
+ const release = (ev) => {
701
+ if (ev.pointerId === void 0 || ev.pointerId === pointerId)
702
+ end();
703
+ };
704
+ endHold.current = end;
705
+ for (const type of events) button.addEventListener(type, release);
706
+ window.addEventListener("pointerup", release);
707
+ window.addEventListener("blur", end);
708
+ };
709
+ const click = (direction) => (e) => {
710
+ if (e.detail === 0) stepOnce(direction);
711
+ };
712
+ const pickUnit = () => {
713
+ endHold.current?.();
714
+ const next = units[(units.indexOf(unit) + 1) % units.length];
715
+ const text = draft.trim();
716
+ if (!BARE_NUMBER.test(text)) return setPicked(next);
717
+ const normal = normaliseLength(text, units, negative, next);
718
+ if (normal === null) return setInvalid(true);
719
+ setPicked(next);
720
+ commit.current(normal);
721
+ };
722
+ const unitName = (u) => u || labels.noUnit;
723
+ const readable = draft.trim() === "" || BARE_NUMBER.test(draft.trim());
724
+ 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";
725
+ return /* @__PURE__ */ jsxs2(
726
+ "div",
727
+ {
728
+ className: cn(
729
+ "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",
730
+ invalid && "border-red-400"
731
+ ),
732
+ children: [
733
+ /* @__PURE__ */ jsx3(
734
+ "input",
735
+ {
736
+ 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",
737
+ value: draft,
738
+ placeholder: fallback ? fallback.amount : placeholder,
739
+ inputMode: "decimal",
740
+ ...fieldLabel,
741
+ "aria-labelledby": labelledBy ?? fieldLabel["aria-labelledby"],
742
+ "aria-invalid": invalid,
743
+ onChange: (e) => {
744
+ endHold.current?.();
745
+ setDraft(e.target.value);
746
+ },
747
+ onBlur: done,
748
+ onKeyDown: onKey
749
+ }
750
+ ),
751
+ readable && (units.length > 1 && !compact ? /* @__PURE__ */ jsx3(
752
+ "button",
753
+ {
754
+ type: "button",
755
+ onClick: pickUnit,
756
+ title: labels.changeUnit(unitName(unit)),
757
+ "aria-label": labels.changeUnit(unitName(unit)),
758
+ 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",
759
+ children: unitName(unit)
760
+ }
761
+ ) : (
762
+ // Narrow, the unit gives its place to the arrows while they show; a unit is typed.
763
+ /* @__PURE__ */ jsx3(
764
+ "span",
765
+ {
766
+ "aria-hidden": true,
767
+ className: cn(
768
+ "shrink-0 px-0.5 font-mono text-[10px] text-panel-muted-fg",
769
+ compact && "group-focus-within/length:hidden group-hover/length:hidden"
770
+ ),
771
+ children: unitName(unit)
772
+ }
773
+ )
774
+ )),
775
+ /* @__PURE__ */ jsxs2(
776
+ "span",
777
+ {
778
+ className: cn(
779
+ "h-full shrink-0 flex-col border-l border-panel-border",
780
+ compact ? "hidden group-focus-within/length:flex group-hover/length:flex" : "flex"
781
+ ),
782
+ children: [
783
+ /* @__PURE__ */ jsx3(
784
+ "button",
785
+ {
786
+ type: "button",
787
+ tabIndex: -1,
788
+ "aria-label": labels.increase,
789
+ onPointerDown: press(1),
790
+ onClick: click(1),
791
+ className: arrow,
792
+ children: /* @__PURE__ */ jsx3(ChevronUp, { className: "size-2.5" })
793
+ }
794
+ ),
795
+ /* @__PURE__ */ jsx3(
796
+ "button",
797
+ {
798
+ type: "button",
799
+ tabIndex: -1,
800
+ "aria-label": labels.decrease,
801
+ onPointerDown: press(-1),
802
+ onClick: click(-1),
803
+ className: arrow,
804
+ children: /* @__PURE__ */ jsx3(ChevronDown, { className: "size-2.5" })
805
+ }
806
+ )
807
+ ]
808
+ }
809
+ )
810
+ ]
811
+ }
812
+ );
813
+ }
814
+ function BoxInput({
815
+ attributes,
816
+ attr,
817
+ placeholder,
818
+ units,
819
+ labels,
820
+ onCommit
821
+ }) {
822
+ const sides = readBox(attributes, attr);
823
+ const defaults = readBox(placeholder ? { [attr]: placeholder } : {}, attr);
824
+ const symmetric = sides[0] === sides[2] && sides[1] === sides[3];
825
+ const [each, setEach] = useState2(!symmetric);
826
+ useEffect2(() => {
827
+ if (!symmetric) setEach(true);
828
+ }, [symmetric]);
829
+ const set = (indices, v, partial) => {
830
+ const next = sides.map((s, i) => s || defaults[i]);
831
+ for (const i of indices) next[i] = v || defaults[i];
832
+ onCommit(next.every((s, i) => s === defaults[i]) ? "" : boxShorthand(next), partial);
833
+ };
834
+ const fieldLabel = useFieldLabel();
835
+ const sideId = useId();
836
+ const sideNames = [labels.sideTop, labels.sideRight, labels.sideBottom, labels.sideLeft];
837
+ const pairs = each ? SIDES.map((_, i) => [sideNames[i], [i]]) : [
838
+ [labels.sidesVertical, [0, 2]],
839
+ [labels.sidesHorizontal, [1, 3]]
840
+ ];
841
+ return /* @__PURE__ */ jsxs2("div", { className: "flex items-start gap-1.5", children: [
842
+ /* @__PURE__ */ jsx3("div", { className: cn("grid flex-1 gap-1.5", each ? "grid-cols-4" : "grid-cols-2"), children: pairs.map(([name, indices]) => /* @__PURE__ */ jsxs2("label", { className: "flex min-w-0 flex-col gap-0.5", title: name, children: [
843
+ /* @__PURE__ */ jsx3(
844
+ LengthInput,
845
+ {
846
+ value: sides[indices[0]],
847
+ placeholder: defaults[indices[0]] || "0",
848
+ units,
849
+ compact: each,
850
+ labels,
851
+ labelledBy: [fieldLabel["aria-labelledby"], `${sideId}-${indices[0]}`].filter(Boolean).join(" "),
852
+ onCommit: (v, partial) => set(indices, v, partial)
853
+ }
854
+ ),
855
+ /* @__PURE__ */ jsx3(
856
+ "span",
857
+ {
858
+ id: `${sideId}-${indices[0]}`,
859
+ className: "truncate text-center text-[10px] text-panel-muted-fg/80",
860
+ children: name
861
+ }
862
+ )
863
+ ] }, name)) }),
864
+ /* @__PURE__ */ jsx3(
865
+ "button",
866
+ {
867
+ type: "button",
868
+ onClick: () => setEach((v) => !v),
869
+ disabled: each && !symmetric,
870
+ "aria-pressed": each,
871
+ title: labels.editEachSide,
872
+ "aria-label": labels.editEachSide,
873
+ className: cn(
874
+ "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",
875
+ each && "border-brand text-panel-fg"
876
+ ),
877
+ children: /* @__PURE__ */ jsx3(Grid2x2, { className: "size-3.5" })
878
+ }
879
+ )
880
+ ] });
881
+ }
882
+ var ALIGN_ICONS = {
883
+ left: /* @__PURE__ */ jsx3(AlignLeft, { className: "size-3.5" }),
884
+ center: /* @__PURE__ */ jsx3(AlignCenter, { className: "size-3.5" }),
885
+ right: /* @__PURE__ */ jsx3(AlignRight, { className: "size-3.5" }),
886
+ justify: /* @__PURE__ */ jsx3(AlignJustify, { className: "size-3.5" })
887
+ };
888
+ function Segmented({
889
+ value,
890
+ fallback,
891
+ options,
892
+ name,
893
+ onCommit
894
+ }) {
895
+ const current = value || fallback;
896
+ const iconsOnly = options.every((o) => o.value in ALIGN_ICONS);
897
+ return /* @__PURE__ */ jsx3("div", { className: "flex h-7 overflow-hidden rounded-md border border-panel-border bg-panel-elevated", children: options.map((o) => {
898
+ const active = current === o.value;
899
+ return /* @__PURE__ */ jsx3(
900
+ "button",
901
+ {
902
+ type: "button",
903
+ "aria-pressed": active,
904
+ title: name(o),
905
+ "aria-label": name(o),
906
+ onClick: () => o.value !== value && onCommit(o.value),
907
+ className: cn(
908
+ "flex min-w-0 flex-1 items-center justify-center px-1 text-[11px] text-panel-muted-fg transition-colors hover:text-panel-fg",
909
+ active && (value ? "bg-brand text-brand-fg hover:text-brand-fg" : "bg-panel-hover text-panel-fg")
910
+ ),
911
+ children: iconsOnly ? ALIGN_ICONS[o.value] : /* @__PURE__ */ jsx3("span", { className: "truncate", children: name(o) })
912
+ },
913
+ o.value
914
+ );
915
+ }) });
916
+ }
917
+ function Select({
918
+ value,
919
+ fallback,
920
+ options,
921
+ name,
922
+ onCommit
923
+ }) {
924
+ const known = !value || options.some((o) => o.value === value);
925
+ const fallbackOption = options.find((o) => o.value === fallback);
926
+ const field = useFieldLabel();
927
+ return /* @__PURE__ */ jsxs2(
928
+ "select",
929
+ {
930
+ className: cn(FIELD, "px-1.5"),
931
+ ...field,
932
+ value: value || "",
933
+ onChange: (e) => onCommit(e.target.value),
934
+ children: [
935
+ /* @__PURE__ */ jsx3("option", { value: "", children: fallbackOption ? name(fallbackOption) : "\u2014" }),
936
+ !known && /* @__PURE__ */ jsx3("option", { value, children: value }),
937
+ options.map((o) => /* @__PURE__ */ jsx3("option", { value: o.value, children: name(o) }, o.value))
938
+ ]
939
+ }
940
+ );
941
+ }
942
+ function Toggle({
943
+ checked,
944
+ label,
945
+ onCommit
946
+ }) {
947
+ return /* @__PURE__ */ jsx3(
948
+ Switch,
949
+ {
950
+ checked,
951
+ onCheckedChange: onCommit,
952
+ "aria-label": label,
953
+ className: "justify-self-end data-checked:bg-brand data-unchecked:bg-panel-border"
954
+ }
955
+ );
956
+ }
957
+
958
+ // src/components/editor/mjml-attributes.ts
959
+ var ALLOWED_ATTRIBUTES = {
960
+ "mj-body": [
961
+ "background-color",
962
+ "width"
963
+ ],
964
+ "mj-wrapper": [
965
+ "background-color",
966
+ "background-position",
967
+ "background-position-x",
968
+ "background-position-y",
969
+ "background-repeat",
970
+ "background-size",
971
+ "background-url",
972
+ "border",
973
+ "border-bottom",
974
+ "border-left",
975
+ "border-radius",
976
+ "border-right",
977
+ "border-top",
978
+ "direction",
979
+ "full-width",
980
+ "gap",
981
+ "padding",
982
+ "padding-bottom",
983
+ "padding-left",
984
+ "padding-right",
985
+ "padding-top",
986
+ "text-align",
987
+ "text-padding"
988
+ ],
989
+ "mj-section": [
990
+ "background-color",
991
+ "background-position",
992
+ "background-position-x",
993
+ "background-position-y",
994
+ "background-repeat",
995
+ "background-size",
996
+ "background-url",
997
+ "border",
998
+ "border-bottom",
999
+ "border-left",
1000
+ "border-radius",
1001
+ "border-right",
1002
+ "border-top",
1003
+ "direction",
1004
+ "full-width",
1005
+ "padding",
1006
+ "padding-bottom",
1007
+ "padding-left",
1008
+ "padding-right",
1009
+ "padding-top",
1010
+ "text-align",
1011
+ "text-padding"
1012
+ ],
1013
+ "mj-column": [
1014
+ "background-color",
1015
+ "border",
1016
+ "border-bottom",
1017
+ "border-left",
1018
+ "border-radius",
1019
+ "border-right",
1020
+ "border-top",
1021
+ "direction",
1022
+ "inner-background-color",
1023
+ "inner-border",
1024
+ "inner-border-bottom",
1025
+ "inner-border-left",
1026
+ "inner-border-radius",
1027
+ "inner-border-right",
1028
+ "inner-border-top",
1029
+ "padding",
1030
+ "padding-bottom",
1031
+ "padding-left",
1032
+ "padding-right",
1033
+ "padding-top",
1034
+ "vertical-align",
1035
+ "width"
1036
+ ],
1037
+ "mj-group": [
1038
+ "background-color",
1039
+ "direction",
1040
+ "vertical-align",
1041
+ "width"
1042
+ ],
1043
+ "mj-text": [
1044
+ "align",
1045
+ "background-color",
1046
+ "color",
1047
+ "container-background-color",
1048
+ "font-family",
1049
+ "font-size",
1050
+ "font-style",
1051
+ "font-weight",
1052
+ "height",
1053
+ "letter-spacing",
1054
+ "line-height",
1055
+ "padding",
1056
+ "padding-bottom",
1057
+ "padding-left",
1058
+ "padding-right",
1059
+ "padding-top",
1060
+ "text-decoration",
1061
+ "text-transform",
1062
+ "vertical-align"
1063
+ ],
1064
+ "mj-button": [
1065
+ "align",
1066
+ "background-color",
1067
+ "border",
1068
+ "border-bottom",
1069
+ "border-left",
1070
+ "border-radius",
1071
+ "border-right",
1072
+ "border-top",
1073
+ "color",
1074
+ "container-background-color",
1075
+ "font-family",
1076
+ "font-size",
1077
+ "font-style",
1078
+ "font-weight",
1079
+ "height",
1080
+ "href",
1081
+ "inner-padding",
1082
+ "letter-spacing",
1083
+ "line-height",
1084
+ "name",
1085
+ "padding",
1086
+ "padding-bottom",
1087
+ "padding-left",
1088
+ "padding-right",
1089
+ "padding-top",
1090
+ "rel",
1091
+ "target",
1092
+ "text-align",
1093
+ "text-decoration",
1094
+ "text-transform",
1095
+ "title",
1096
+ "vertical-align",
1097
+ "width"
1098
+ ],
1099
+ "mj-image": [
1100
+ "align",
1101
+ "alt",
1102
+ "border",
1103
+ "border-bottom",
1104
+ "border-left",
1105
+ "border-radius",
1106
+ "border-right",
1107
+ "border-top",
1108
+ "container-background-color",
1109
+ "fluid-on-mobile",
1110
+ "font-size",
1111
+ "height",
1112
+ "href",
1113
+ "max-height",
1114
+ "name",
1115
+ "padding",
1116
+ "padding-bottom",
1117
+ "padding-left",
1118
+ "padding-right",
1119
+ "padding-top",
1120
+ "rel",
1121
+ "sizes",
1122
+ "src",
1123
+ "srcset",
1124
+ "target",
1125
+ "title",
1126
+ "usemap",
1127
+ "width"
1128
+ ],
1129
+ "mj-divider": [
1130
+ "align",
1131
+ "border-color",
1132
+ "border-style",
1133
+ "border-width",
1134
+ "container-background-color",
1135
+ "padding",
1136
+ "padding-bottom",
1137
+ "padding-left",
1138
+ "padding-right",
1139
+ "padding-top",
1140
+ "width"
1141
+ ],
1142
+ "mj-spacer": [
1143
+ "border",
1144
+ "border-bottom",
1145
+ "border-left",
1146
+ "border-right",
1147
+ "border-top",
1148
+ "container-background-color",
1149
+ "height",
1150
+ "padding",
1151
+ "padding-bottom",
1152
+ "padding-left",
1153
+ "padding-right",
1154
+ "padding-top"
1155
+ ],
1156
+ "mj-social": [
1157
+ "align",
1158
+ "border-radius",
1159
+ "color",
1160
+ "container-background-color",
1161
+ "font-family",
1162
+ "font-size",
1163
+ "font-style",
1164
+ "font-weight",
1165
+ "icon-height",
1166
+ "icon-padding",
1167
+ "icon-size",
1168
+ "inner-padding",
1169
+ "line-height",
1170
+ "mode",
1171
+ "padding",
1172
+ "padding-bottom",
1173
+ "padding-left",
1174
+ "padding-right",
1175
+ "padding-top",
1176
+ "table-layout",
1177
+ "text-decoration",
1178
+ "text-padding",
1179
+ "vertical-align"
1180
+ ],
1181
+ "mj-social-element": [
1182
+ "align",
1183
+ "alt",
1184
+ "background-color",
1185
+ "border-radius",
1186
+ "color",
1187
+ "font-family",
1188
+ "font-size",
1189
+ "font-style",
1190
+ "font-weight",
1191
+ "href",
1192
+ "icon-height",
1193
+ "icon-padding",
1194
+ "icon-position",
1195
+ "icon-size",
1196
+ "line-height",
1197
+ "name",
1198
+ "padding",
1199
+ "padding-bottom",
1200
+ "padding-left",
1201
+ "padding-right",
1202
+ "padding-top",
1203
+ "rel",
1204
+ "sizes",
1205
+ "src",
1206
+ "srcset",
1207
+ "target",
1208
+ "text-decoration",
1209
+ "text-padding",
1210
+ "title",
1211
+ "vertical-align"
1212
+ ],
1213
+ "mj-hero": [
1214
+ "background-color",
1215
+ "background-height",
1216
+ "background-position",
1217
+ "background-url",
1218
+ "background-width",
1219
+ "border-radius",
1220
+ "container-background-color",
1221
+ "height",
1222
+ "inner-background-color",
1223
+ "inner-padding",
1224
+ "inner-padding-bottom",
1225
+ "inner-padding-left",
1226
+ "inner-padding-right",
1227
+ "inner-padding-top",
1228
+ "mode",
1229
+ "padding",
1230
+ "padding-bottom",
1231
+ "padding-left",
1232
+ "padding-right",
1233
+ "padding-top",
1234
+ "vertical-align"
1235
+ ],
1236
+ "mj-navbar": [
1237
+ "align",
1238
+ "base-url",
1239
+ "hamburger",
1240
+ "ico-align",
1241
+ "ico-close",
1242
+ "ico-color",
1243
+ "ico-font-family",
1244
+ "ico-font-size",
1245
+ "ico-line-height",
1246
+ "ico-open",
1247
+ "ico-padding",
1248
+ "ico-padding-bottom",
1249
+ "ico-padding-left",
1250
+ "ico-padding-right",
1251
+ "ico-padding-top",
1252
+ "ico-text-decoration",
1253
+ "ico-text-transform",
1254
+ "padding",
1255
+ "padding-bottom",
1256
+ "padding-left",
1257
+ "padding-right",
1258
+ "padding-top"
1259
+ ],
1260
+ "mj-navbar-link": [
1261
+ "color",
1262
+ "font-family",
1263
+ "font-size",
1264
+ "font-style",
1265
+ "font-weight",
1266
+ "href",
1267
+ "letter-spacing",
1268
+ "line-height",
1269
+ "name",
1270
+ "padding",
1271
+ "padding-bottom",
1272
+ "padding-left",
1273
+ "padding-right",
1274
+ "padding-top",
1275
+ "rel",
1276
+ "target",
1277
+ "text-decoration",
1278
+ "text-transform"
1279
+ ]
1280
+ };
1281
+ var ALWAYS_ALLOWED = /* @__PURE__ */ new Set(["mj-class", "css-class", "id", "style"]);
1282
+ var START_TAG = /<[a-z][^\s/>]*(?:[^>"']|"[^"]*"|'[^']*')*>/gi;
1283
+ var ATTRIBUTE = /\s+([^\s"'>/=]+)(?:\s*=\s*(?:"[^"]*"|'[^']*'|[^\s"'>]+))?/g;
1284
+ function withoutEditorProps(mjml) {
1285
+ return mjml.replace(
1286
+ START_TAG,
1287
+ (tag) => tag.replace(
1288
+ ATTRIBUTE,
1289
+ (attribute, name) => name.toLowerCase().startsWith("data-gjs-") ? "" : attribute
1290
+ )
1291
+ );
1292
+ }
1293
+ function withClosedTags(mjml) {
1294
+ return mjml.replace(START_TAG, (tag) => {
1295
+ const selfClosed = /^<(mj-[\w-]+)([\s\S]*?)\s*\/>$/i.exec(tag);
1296
+ return selfClosed ? `<${selfClosed[1]}${selfClosed[2]}></${selfClosed[1]}>` : tag;
1297
+ });
1298
+ }
1299
+ var TABLE = /<!--[\s\S]*?(?:-->|$)|(<mj-table\b(?:[^>"']|"[^"]*"|'[^']*')*>)((?:<!--[\s\S]*?-->|(?!<!--)[\s\S])*?)(<\/mj-table\s*>)/gi;
1300
+ function withTableRowsKept(mjml) {
1301
+ return mjml.replace(
1302
+ TABLE,
1303
+ (match, open, rows, close) => open ? `${open}<template>${rows}</template>${close}` : match
1304
+ );
1305
+ }
1306
+ function sourceForEditor(mjml) {
1307
+ const cleaned = withoutEditorProps(mjml);
1308
+ return { mjml: withTableRowsKept(withClosedTags(cleaned)), repaired: cleaned !== mjml };
1309
+ }
1310
+ function illegalAttributes(tag, attributes) {
1311
+ const allowed = ALLOWED_ATTRIBUTES[tag];
1312
+ if (!allowed) return [];
1313
+ return Object.keys(attributes).filter((a) => !ALWAYS_ALLOWED.has(a) && !allowed.includes(a));
1314
+ }
1315
+ var NAMED_COLORS = /* @__PURE__ */ new Set([
1316
+ "aliceblue",
1317
+ "antiquewhite",
1318
+ "aqua",
1319
+ "aquamarine",
1320
+ "azure",
1321
+ "beige",
1322
+ "bisque",
1323
+ "black",
1324
+ "blanchedalmond",
1325
+ "blue",
1326
+ "blueviolet",
1327
+ "brown",
1328
+ "burlywood",
1329
+ "cadetblue",
1330
+ "chartreuse",
1331
+ "chocolate",
1332
+ "coral",
1333
+ "cornflowerblue",
1334
+ "cornsilk",
1335
+ "crimson",
1336
+ "cyan",
1337
+ "darkblue",
1338
+ "darkcyan",
1339
+ "darkgoldenrod",
1340
+ "darkgray",
1341
+ "darkgreen",
1342
+ "darkgrey",
1343
+ "darkkhaki",
1344
+ "darkmagenta",
1345
+ "darkolivegreen",
1346
+ "darkorange",
1347
+ "darkorchid",
1348
+ "darkred",
1349
+ "darksalmon",
1350
+ "darkseagreen",
1351
+ "darkslateblue",
1352
+ "darkslategray",
1353
+ "darkslategrey",
1354
+ "darkturquoise",
1355
+ "darkviolet",
1356
+ "deeppink",
1357
+ "deepskyblue",
1358
+ "dimgray",
1359
+ "dimgrey",
1360
+ "dodgerblue",
1361
+ "firebrick",
1362
+ "floralwhite",
1363
+ "forestgreen",
1364
+ "fuchsia",
1365
+ "gainsboro",
1366
+ "ghostwhite",
1367
+ "gold",
1368
+ "goldenrod",
1369
+ "gray",
1370
+ "green",
1371
+ "greenyellow",
1372
+ "grey",
1373
+ "honeydew",
1374
+ "hotpink",
1375
+ "indianred",
1376
+ "indigo",
1377
+ "inherit",
1378
+ "ivory",
1379
+ "khaki",
1380
+ "lavender",
1381
+ "lavenderblush",
1382
+ "lawngreen",
1383
+ "lemonchiffon",
1384
+ "lightblue",
1385
+ "lightcoral",
1386
+ "lightcyan",
1387
+ "lightgoldenrodyellow",
1388
+ "lightgray",
1389
+ "lightgreen",
1390
+ "lightgrey",
1391
+ "lightpink",
1392
+ "lightsalmon",
1393
+ "lightseagreen",
1394
+ "lightskyblue",
1395
+ "lightslategray",
1396
+ "lightslategrey",
1397
+ "lightsteelblue",
1398
+ "lightyellow",
1399
+ "lime",
1400
+ "limegreen",
1401
+ "linen",
1402
+ "magenta",
1403
+ "maroon",
1404
+ "mediumaquamarine",
1405
+ "mediumblue",
1406
+ "mediumorchid",
1407
+ "mediumpurple",
1408
+ "mediumseagreen",
1409
+ "mediumslateblue",
1410
+ "mediumspringgreen",
1411
+ "mediumturquoise",
1412
+ "mediumvioletred",
1413
+ "midnightblue",
1414
+ "mintcream",
1415
+ "mistyrose",
1416
+ "moccasin",
1417
+ "navajowhite",
1418
+ "navy",
1419
+ "oldlace",
1420
+ "olive",
1421
+ "olivedrab",
1422
+ "orange",
1423
+ "orangered",
1424
+ "orchid",
1425
+ "palegoldenrod",
1426
+ "palegreen",
1427
+ "paleturquoise",
1428
+ "palevioletred",
1429
+ "papayawhip",
1430
+ "peachpuff",
1431
+ "peru",
1432
+ "pink",
1433
+ "plum",
1434
+ "powderblue",
1435
+ "purple",
1436
+ "rebeccapurple",
1437
+ "red",
1438
+ "rosybrown",
1439
+ "royalblue",
1440
+ "saddlebrown",
1441
+ "salmon",
1442
+ "sandybrown",
1443
+ "seagreen",
1444
+ "seashell",
1445
+ "sienna",
1446
+ "silver",
1447
+ "skyblue",
1448
+ "slateblue",
1449
+ "slategray",
1450
+ "slategrey",
1451
+ "snow",
1452
+ "springgreen",
1453
+ "steelblue",
1454
+ "tan",
1455
+ "teal",
1456
+ "thistle",
1457
+ "tomato",
1458
+ "transparent",
1459
+ "turquoise",
1460
+ "violet",
1461
+ "wheat",
1462
+ "white",
1463
+ "whitesmoke",
1464
+ "yellow",
1465
+ "yellowgreen"
1466
+ ]);
1467
+
1468
+ // src/components/editor/ColorField.tsx
1469
+ import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
361
1470
  var PRESETS = [
362
1471
  "#000000",
363
1472
  "#374151",
@@ -378,32 +1487,33 @@ var eyeDropper = () => globalThis.EyeDropper;
378
1487
  function ColorField({
379
1488
  value,
380
1489
  placeholder,
381
- documentColors,
1490
+ documentColors: documentColors2,
382
1491
  labels,
383
1492
  onChange
384
1493
  }) {
385
- const [text, setText] = useState2(value);
386
- const [open, setOpen] = useState2(false);
387
- const [used, setUsed] = useState2([]);
388
- const pressed = useRef2(false);
389
- const dragged = useRef2(null);
390
- const fieldRef = useRef2(null);
391
- const anchor = useRef2({
1494
+ const field = useFieldLabel();
1495
+ const [text, setText] = useState3(value);
1496
+ const [open, setOpen] = useState3(false);
1497
+ const [used, setUsed] = useState3([]);
1498
+ const pressed = useRef3(false);
1499
+ const dragged = useRef3(null);
1500
+ const fieldRef = useRef3(null);
1501
+ const anchor = useRef3({
392
1502
  getBoundingClientRect: () => {
393
- const field = fieldRef.current?.getBoundingClientRect() ?? new DOMRect();
1503
+ const field2 = fieldRef.current?.getBoundingClientRect() ?? new DOMRect();
394
1504
  const panel = fieldRef.current?.closest(".editor-dark")?.getBoundingClientRect();
395
- return new DOMRect(panel?.right ?? field.right, field.top, 0, field.height);
1505
+ return new DOMRect(panel?.right ?? field2.right, field2.top, 0, field2.height);
396
1506
  }
397
1507
  });
398
- useEffect2(() => setText(value), [value]);
399
- const latestOnChange = useRef2(onChange);
1508
+ useEffect3(() => setText(value), [value]);
1509
+ const latestOnChange = useRef3(onChange);
400
1510
  latestOnChange.current = onChange;
401
1511
  const release = useCallback2(() => {
402
1512
  pressed.current = false;
403
1513
  if (dragged.current !== null) latestOnChange.current(dragged.current, false);
404
1514
  dragged.current = null;
405
1515
  }, []);
406
- useEffect2(() => {
1516
+ useEffect3(() => {
407
1517
  document.addEventListener("lostpointercapture", release);
408
1518
  return () => {
409
1519
  document.removeEventListener("lostpointercapture", release);
@@ -412,12 +1522,16 @@ function ColorField({
412
1522
  }, [release]);
413
1523
  const hex = toHex(value) ?? toHex(placeholder ?? "") ?? "#000000";
414
1524
  const translucent = hex.length === 9;
415
- const empty = !value;
1525
+ const shown = value || placeholder || "";
416
1526
  const commit = (next) => {
417
1527
  dragged.current = null;
418
- onChange(next, false);
1528
+ const color = mjmlColor(next);
1529
+ if (color === null) return setText(value);
1530
+ setText(color);
1531
+ onChange(color, false);
419
1532
  };
420
- const drag = (next) => {
1533
+ const drag = (picked) => {
1534
+ const next = mjmlColor(picked) ?? picked;
421
1535
  setText(next);
422
1536
  if (!pressed.current) return commit(next);
423
1537
  dragged.current = next;
@@ -432,29 +1546,30 @@ function ColorField({
432
1546
  } catch {
433
1547
  }
434
1548
  };
435
- return /* @__PURE__ */ jsxs2(
1549
+ return /* @__PURE__ */ jsxs3(
436
1550
  Popover,
437
1551
  {
438
1552
  open,
439
1553
  onOpenChange: (next) => {
440
- if (next) setUsed(documentColors());
1554
+ if (next) setUsed(documentColors2());
441
1555
  else if (text.trim() !== value) commit(text.trim());
442
1556
  setOpen(next);
443
1557
  },
444
1558
  children: [
445
- /* @__PURE__ */ jsx3(PopoverAnchor, { virtualRef: anchor }),
446
- /* @__PURE__ */ jsxs2(
1559
+ /* @__PURE__ */ jsx4(PopoverAnchor, { virtualRef: anchor }),
1560
+ /* @__PURE__ */ jsxs3(
447
1561
  "div",
448
1562
  {
449
1563
  ref: fieldRef,
450
1564
  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",
451
1565
  children: [
452
- /* @__PURE__ */ jsx3(
1566
+ /* @__PURE__ */ jsx4(
453
1567
  "input",
454
1568
  {
455
1569
  value: text,
456
1570
  placeholder,
457
1571
  spellCheck: false,
1572
+ ...field,
458
1573
  onChange: (e) => setText(e.target.value),
459
1574
  onBlur: () => text !== value && commit(text.trim()),
460
1575
  onKeyDown: (e) => {
@@ -464,7 +1579,7 @@ function ColorField({
464
1579
  className: "min-w-0 flex-1 bg-transparent font-mono text-xs text-panel-fg outline-none placeholder:text-panel-muted-fg"
465
1580
  }
466
1581
  ),
467
- /* @__PURE__ */ jsx3(PopoverTrigger, { asChild: true, children: /* @__PURE__ */ jsxs2(
1582
+ /* @__PURE__ */ jsx4(PopoverTrigger, { asChild: true, children: /* @__PURE__ */ jsxs3(
468
1583
  "button",
469
1584
  {
470
1585
  type: "button",
@@ -472,15 +1587,15 @@ function ColorField({
472
1587
  "aria-label": labels.colorPickerOpen,
473
1588
  className: "relative size-5 shrink-0 overflow-hidden rounded border border-white/20 outline-none focus-visible:ring-2 focus-visible:ring-brand",
474
1589
  children: [
475
- /* @__PURE__ */ jsx3(Checkerboard, {}),
476
- !empty && /* @__PURE__ */ jsx3("span", { className: "absolute inset-0", style: { background: value } })
1590
+ /* @__PURE__ */ jsx4(Checkerboard, {}),
1591
+ mjmlColor(shown) && /* @__PURE__ */ jsx4("span", { className: "absolute inset-0", style: { background: shown } })
477
1592
  ]
478
1593
  }
479
1594
  ) })
480
1595
  ]
481
1596
  }
482
1597
  ),
483
- /* @__PURE__ */ jsxs2(
1598
+ /* @__PURE__ */ jsxs3(
484
1599
  PopoverContent,
485
1600
  {
486
1601
  side: "right",
@@ -488,36 +1603,37 @@ function ColorField({
488
1603
  sideOffset: 8,
489
1604
  className: "editor-dark flex w-52 flex-col gap-2.5 border border-panel-border bg-panel p-2.5 text-panel-fg",
490
1605
  children: [
491
- /* @__PURE__ */ jsx3(
1606
+ /* @__PURE__ */ jsx4(
492
1607
  "div",
493
1608
  {
494
1609
  onPointerDownCapture: (e) => {
495
1610
  pressed.current = true;
496
1611
  e.target.setPointerCapture(e.pointerId);
497
1612
  },
498
- children: translucent ? /* @__PURE__ */ jsx3(HexAlphaColorPicker, { color: hex, onChange: drag, className: "color-field-picker" }) : /* @__PURE__ */ jsx3(HexColorPicker, { color: hex, onChange: drag, className: "color-field-picker" })
1613
+ children: translucent ? /* @__PURE__ */ jsx4(HexAlphaColorPicker, { color: hex, onChange: drag, className: "color-field-picker" }) : /* @__PURE__ */ jsx4(HexColorPicker, { color: hex, onChange: drag, className: "color-field-picker" })
499
1614
  }
500
1615
  ),
501
- /* @__PURE__ */ jsxs2("div", { className: "flex items-center gap-1.5", children: [
502
- /* @__PURE__ */ jsx3(
1616
+ /* @__PURE__ */ jsxs3("div", { className: "flex items-center gap-1.5", children: [
1617
+ /* @__PURE__ */ jsx4(
503
1618
  "span",
504
1619
  {
505
1620
  className: "size-6 shrink-0 rounded border border-white/20",
506
1621
  style: { background: hex }
507
1622
  }
508
1623
  ),
509
- /* @__PURE__ */ jsx3(
1624
+ /* @__PURE__ */ jsx4(
510
1625
  "input",
511
1626
  {
512
1627
  value: text,
513
1628
  spellCheck: false,
1629
+ ...field,
514
1630
  onChange: (e) => setText(e.target.value),
515
1631
  onKeyDown: (e) => e.key === "Enter" && commit(text.trim()),
516
1632
  onBlur: () => text !== value && commit(text.trim()),
517
1633
  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"
518
1634
  }
519
1635
  ),
520
- eyeDropper() && /* @__PURE__ */ jsx3(
1636
+ eyeDropper() && /* @__PURE__ */ jsx4(
521
1637
  "button",
522
1638
  {
523
1639
  type: "button",
@@ -525,11 +1641,11 @@ function ColorField({
525
1641
  title: labels.colorPickerEyedropper,
526
1642
  "aria-label": labels.colorPickerEyedropper,
527
1643
  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",
528
- children: /* @__PURE__ */ jsx3(Pipette, { className: "size-3" })
1644
+ children: /* @__PURE__ */ jsx4(Pipette, { className: "size-3" })
529
1645
  }
530
1646
  )
531
1647
  ] }),
532
- used.length > 0 && /* @__PURE__ */ jsx3(
1648
+ used.length > 0 && /* @__PURE__ */ jsx4(
533
1649
  Swatches,
534
1650
  {
535
1651
  title: labels.colorPickerDocument,
@@ -538,7 +1654,7 @@ function ColorField({
538
1654
  onPick: commit
539
1655
  }
540
1656
  ),
541
- /* @__PURE__ */ jsx3(
1657
+ /* @__PURE__ */ jsx4(
542
1658
  Swatches,
543
1659
  {
544
1660
  title: labels.colorPickerPresets,
@@ -562,10 +1678,10 @@ function Swatches({
562
1678
  onPick,
563
1679
  transparentLabel
564
1680
  }) {
565
- return /* @__PURE__ */ jsxs2("div", { children: [
566
- /* @__PURE__ */ jsx3("p", { className: "mb-1 text-[10px] font-medium tracking-wide text-panel-muted-fg uppercase", children: title }),
567
- /* @__PURE__ */ jsxs2("div", { className: "grid grid-cols-8 gap-1", children: [
568
- colors.map((color) => /* @__PURE__ */ jsx3(
1681
+ return /* @__PURE__ */ jsxs3("div", { children: [
1682
+ /* @__PURE__ */ jsx4("p", { className: "mb-1 text-[10px] font-medium tracking-wide text-panel-muted-fg uppercase", children: title }),
1683
+ /* @__PURE__ */ jsxs3("div", { className: "grid grid-cols-8 gap-1", children: [
1684
+ colors.map((color) => /* @__PURE__ */ jsx4(
569
1685
  "button",
570
1686
  {
571
1687
  type: "button",
@@ -579,21 +1695,21 @@ function Swatches({
579
1695
  },
580
1696
  color
581
1697
  )),
582
- transparentLabel && /* @__PURE__ */ jsx3(
1698
+ transparentLabel && /* @__PURE__ */ jsx4(
583
1699
  "button",
584
1700
  {
585
1701
  type: "button",
586
1702
  title: transparentLabel,
587
1703
  onClick: () => onPick("transparent"),
588
1704
  className: "relative aspect-square overflow-hidden rounded border border-white/15 outline-none hover:scale-110 focus-visible:ring-2 focus-visible:ring-brand",
589
- children: /* @__PURE__ */ jsx3(Checkerboard, {})
1705
+ children: /* @__PURE__ */ jsx4(Checkerboard, {})
590
1706
  }
591
1707
  )
592
1708
  ] })
593
1709
  ] });
594
1710
  }
595
1711
  function Checkerboard() {
596
- return /* @__PURE__ */ jsx3(
1712
+ return /* @__PURE__ */ jsx4(
597
1713
  "span",
598
1714
  {
599
1715
  className: "absolute inset-0",
@@ -604,6 +1720,16 @@ function Checkerboard() {
604
1720
  }
605
1721
  );
606
1722
  }
1723
+ function mjmlColor(value) {
1724
+ const v = value.trim();
1725
+ if (v === "") return "";
1726
+ if (NAMED_COLORS.has(v.toLowerCase())) return v.toLowerCase();
1727
+ const hex = toHex(v);
1728
+ if (!hex) return null;
1729
+ if (hex.length === 7) return hex;
1730
+ const [r, g, b, a] = [1, 3, 5, 7].map((i) => parseInt(hex.slice(i, i + 2), 16));
1731
+ return `rgba(${r},${g},${b},${Number((a / 255).toFixed(2))})`;
1732
+ }
607
1733
  function toHex(value, { opaque = false } = {}) {
608
1734
  const v = value.trim().toLowerCase();
609
1735
  const short = /^#([0-9a-f])([0-9a-f])([0-9a-f])$/.exec(v);
@@ -623,7 +1749,7 @@ function toHex(value, { opaque = false } = {}) {
623
1749
 
624
1750
  // src/components/editor/ImageField.tsx
625
1751
  import { ImageIcon, X } from "lucide-react";
626
- import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
1752
+ import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
627
1753
  function ImageField({
628
1754
  value,
629
1755
  labels,
@@ -631,8 +1757,8 @@ function ImageField({
631
1757
  onClear
632
1758
  }) {
633
1759
  const url = imageUrl(value);
634
- return /* @__PURE__ */ jsxs3("div", { className: "relative", children: [
635
- /* @__PURE__ */ jsxs3(
1760
+ return /* @__PURE__ */ jsxs4("div", { className: "relative", children: [
1761
+ /* @__PURE__ */ jsxs4(
636
1762
  "button",
637
1763
  {
638
1764
  type: "button",
@@ -640,15 +1766,15 @@ function ImageField({
640
1766
  title: labels.chooseImage,
641
1767
  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",
642
1768
  children: [
643
- url ? /* @__PURE__ */ jsx4("img", { src: url, alt: "", className: "h-24 w-full bg-black/25 object-contain" }) : /* @__PURE__ */ jsxs3("span", { className: "flex h-16 flex-col items-center justify-center gap-1 text-xs text-panel-muted-fg", children: [
644
- /* @__PURE__ */ jsx4(ImageIcon, { className: "size-5" }),
1769
+ url ? /* @__PURE__ */ jsx5("img", { src: url, alt: "", className: "h-24 w-full bg-black/25 object-contain" }) : /* @__PURE__ */ jsxs4("span", { className: "flex h-16 flex-col items-center justify-center gap-1 text-xs text-panel-muted-fg", children: [
1770
+ /* @__PURE__ */ jsx5(ImageIcon, { className: "size-5" }),
645
1771
  labels.chooseImage
646
1772
  ] }),
647
- url && /* @__PURE__ */ jsx4("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 })
1773
+ url && /* @__PURE__ */ jsx5("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 })
648
1774
  ]
649
1775
  }
650
1776
  ),
651
- value && /* @__PURE__ */ jsx4(
1777
+ value && /* @__PURE__ */ jsx5(
652
1778
  "button",
653
1779
  {
654
1780
  type: "button",
@@ -656,7 +1782,7 @@ function ImageField({
656
1782
  title: labels.imageFieldRemove,
657
1783
  "aria-label": labels.imageFieldRemove,
658
1784
  className: "absolute top-1 right-1 flex size-5 items-center justify-center rounded bg-black/60 text-white hover:bg-black/80",
659
- children: /* @__PURE__ */ jsx4(X, { className: "size-3" })
1785
+ children: /* @__PURE__ */ jsx5(X, { className: "size-3" })
660
1786
  }
661
1787
  )
662
1788
  ] });
@@ -669,44 +1795,1530 @@ function imageUrl(value) {
669
1795
  return /^[a-z-]+\(/i.test(v) ? null : v;
670
1796
  }
671
1797
 
672
- // src/components/editor/EmailEditor.tsx
673
- import { createRoot } from "react-dom/client";
674
- import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
675
- var SEC_ID_RE = /\bsec-([A-Za-z0-9_-]+)\b/;
676
- var OBJ_ID_RE = /\bobj-([A-Za-z0-9_-]+)\b/;
677
- var TYPE_LABEL = {
678
- "mj-text": "Text",
679
- "mj-button": "Button",
680
- "mj-image": "Image",
681
- "mj-column": "Column"
1798
+ // src/components/editor/inspector/schema.ts
1799
+ var PX = ["px"];
1800
+ var PX_PCT = ["px", "%"];
1801
+ var LINE_HEIGHT = ["", "px", "%"];
1802
+ var LETTER_SPACING = ["px", "em"];
1803
+ var ALIGN = [
1804
+ { value: "left", label: "alignLeft" },
1805
+ { value: "center", label: "alignCenter" },
1806
+ { value: "right", label: "alignRight" }
1807
+ ];
1808
+ var VERTICAL_ALIGN = [
1809
+ { value: "top", label: "alignTop" },
1810
+ { value: "middle", label: "alignMiddle" },
1811
+ { value: "bottom", label: "alignBottom" }
1812
+ ];
1813
+ var FONTS = [
1814
+ { value: "Ubuntu, Helvetica, Arial, sans-serif", label: "Ubuntu" },
1815
+ { value: "Arial, Helvetica, sans-serif", label: "Arial" },
1816
+ { value: "Helvetica, Arial, sans-serif", label: "Helvetica" },
1817
+ { value: "Verdana, Geneva, sans-serif", label: "Verdana" },
1818
+ { value: "Tahoma, Geneva, sans-serif", label: "Tahoma" },
1819
+ { value: "'Trebuchet MS', Helvetica, sans-serif", label: "Trebuchet MS" },
1820
+ { value: "Georgia, 'Times New Roman', serif", label: "Georgia" },
1821
+ { value: "'Times New Roman', Times, serif", label: "Times New Roman" },
1822
+ { value: "'Courier New', Courier, monospace", label: "Courier New" }
1823
+ ];
1824
+ var WEIGHTS = [
1825
+ { value: "normal", label: "weightRegular" },
1826
+ { value: "500", label: "weightMedium" },
1827
+ { value: "600", label: "weightSemibold" },
1828
+ { value: "bold", label: "weightBold" }
1829
+ ];
1830
+ var TEXT_TRANSFORM = [
1831
+ { value: "none", label: "caseNone" },
1832
+ { value: "uppercase", label: "caseUpper" },
1833
+ { value: "lowercase", label: "caseLower" },
1834
+ { value: "capitalize", label: "caseCapitalize" }
1835
+ ];
1836
+ var TARGET = [
1837
+ { value: "_blank", label: "targetNewTab" },
1838
+ { value: "_self", label: "targetSameTab" }
1839
+ ];
1840
+ var BACKGROUND_POSITION = [
1841
+ { value: "top left", label: "positionTopLeft" },
1842
+ { value: "top center", label: "positionTopCenter" },
1843
+ { value: "top right", label: "positionTopRight" },
1844
+ { value: "center left", label: "positionCenterLeft" },
1845
+ { value: "center center", label: "positionCenter" },
1846
+ { value: "center right", label: "positionCenterRight" },
1847
+ { value: "bottom left", label: "positionBottomLeft" },
1848
+ { value: "bottom center", label: "positionBottomCenter" },
1849
+ { value: "bottom right", label: "positionBottomRight" }
1850
+ ];
1851
+ var NETWORKS = [
1852
+ { value: "facebook", label: "Facebook" },
1853
+ { value: "instagram", label: "Instagram" },
1854
+ { value: "x", label: "X" },
1855
+ { value: "linkedin", label: "LinkedIn" },
1856
+ { value: "youtube", label: "YouTube" },
1857
+ { value: "pinterest", label: "Pinterest" },
1858
+ { value: "github", label: "GitHub" },
1859
+ { value: "web", label: "networkWebsite" },
1860
+ { value: "snapchat", label: "Snapchat" },
1861
+ { value: "vimeo", label: "Vimeo" },
1862
+ { value: "medium", label: "Medium" },
1863
+ { value: "dribbble", label: "Dribbble" },
1864
+ { value: "soundcloud", label: "SoundCloud" },
1865
+ { value: "tumblr", label: "Tumblr" },
1866
+ { value: "xing", label: "Xing" },
1867
+ { value: "custom", label: "networkCustom" }
1868
+ ];
1869
+ var hasBackgroundImage = (a) => !!a["background-url"];
1870
+ var SECTION_FIELDS = [
1871
+ {
1872
+ attr: "full-width",
1873
+ label: "fullWidth",
1874
+ group: "layout",
1875
+ control: { type: "toggle", on: "full-width" }
1876
+ },
1877
+ {
1878
+ attr: "padding",
1879
+ label: "padding",
1880
+ group: "spacing",
1881
+ control: { type: "box", units: PX_PCT },
1882
+ default: "20px 0"
1883
+ },
1884
+ {
1885
+ attr: "background-color",
1886
+ label: "backgroundColor",
1887
+ group: "style",
1888
+ control: { type: "color" }
1889
+ },
1890
+ { attr: "background-url", label: "backgroundImage", group: "style", control: { type: "image" } },
1891
+ {
1892
+ attr: "background-size",
1893
+ label: "backgroundSize",
1894
+ group: "style",
1895
+ control: {
1896
+ type: "segmented",
1897
+ options: [
1898
+ { value: "cover", label: "sizeCover" },
1899
+ { value: "contain", label: "sizeContain" },
1900
+ { value: "auto", label: "sizeAuto" }
1901
+ ]
1902
+ },
1903
+ default: "auto",
1904
+ when: hasBackgroundImage
1905
+ },
1906
+ {
1907
+ attr: "background-repeat",
1908
+ label: "backgroundRepeat",
1909
+ group: "style",
1910
+ control: {
1911
+ type: "segmented",
1912
+ options: [
1913
+ { value: "repeat", label: "repeatTile" },
1914
+ { value: "no-repeat", label: "repeatOnce" }
1915
+ ]
1916
+ },
1917
+ default: "repeat",
1918
+ when: hasBackgroundImage
1919
+ },
1920
+ {
1921
+ attr: "background-position",
1922
+ label: "backgroundPosition",
1923
+ group: "style",
1924
+ control: { type: "select", options: BACKGROUND_POSITION },
1925
+ default: "top center",
1926
+ when: hasBackgroundImage
1927
+ },
1928
+ {
1929
+ attr: "border-radius",
1930
+ label: "cornerRadius",
1931
+ group: "style",
1932
+ control: { type: "length", units: PX_PCT }
1933
+ }
1934
+ ];
1935
+ var SCHEMA = {
1936
+ "mj-body": [
1937
+ { attr: "width", label: "contentWidth", group: "layout", control: { type: "length", units: PX }, default: "600px" },
1938
+ { attr: "background-color", label: "pageBackground", group: "style", control: { type: "color" } }
1939
+ ],
1940
+ "mj-wrapper": [
1941
+ ...SECTION_FIELDS,
1942
+ { attr: "gap", label: "gap", group: "advanced", control: { type: "length", units: PX } },
1943
+ { attr: "border", label: "border", group: "advanced", control: { type: "text", placeholder: "1px solid #cccccc" } }
1944
+ ],
1945
+ "mj-section": [
1946
+ ...SECTION_FIELDS,
1947
+ { attr: "text-align", label: "alignColumns", group: "advanced", control: { type: "segmented", options: ALIGN }, default: "center" },
1948
+ { attr: "direction", label: "reverseColumns", group: "advanced", control: { type: "toggle", on: "rtl" } },
1949
+ { attr: "border", label: "border", group: "advanced", control: { type: "text", placeholder: "1px solid #cccccc" } }
1950
+ ],
1951
+ "mj-column": [
1952
+ { attr: "vertical-align", label: "verticalAlign", group: "layout", control: { type: "segmented", options: VERTICAL_ALIGN }, default: "top" },
1953
+ { attr: "padding", label: "padding", group: "spacing", control: { type: "box", units: PX_PCT } },
1954
+ { attr: "background-color", label: "backgroundColor", group: "style", control: { type: "color" } },
1955
+ { attr: "border-radius", label: "cornerRadius", group: "style", control: { type: "length", units: PX_PCT } },
1956
+ { attr: "width", label: "width", group: "advanced", control: { type: "length", units: PX_PCT } },
1957
+ { attr: "inner-background-color", label: "innerBackground", group: "advanced", control: { type: "color" } },
1958
+ { attr: "inner-border-radius", label: "innerCornerRadius", group: "advanced", control: { type: "length", units: PX_PCT } },
1959
+ { attr: "border", label: "border", group: "advanced", control: { type: "text", placeholder: "1px solid #cccccc" } },
1960
+ { attr: "inner-border", label: "innerBorder", group: "advanced", control: { type: "text", placeholder: "1px solid #cccccc" } }
1961
+ ],
1962
+ "mj-text": [
1963
+ {
1964
+ attr: "align",
1965
+ label: "align",
1966
+ group: "layout",
1967
+ control: { type: "segmented", options: [...ALIGN, { value: "justify", label: "alignJustify" }] },
1968
+ default: "left"
1969
+ },
1970
+ { attr: "padding", label: "padding", group: "spacing", control: { type: "box", units: PX_PCT }, default: "10px 25px" },
1971
+ { attr: "font-family", label: "font", group: "style", control: { type: "select", options: FONTS }, default: "Ubuntu, Helvetica, Arial, sans-serif" },
1972
+ { attr: "font-size", label: "fontSize", group: "style", control: { type: "length", units: PX }, default: "13px" },
1973
+ { attr: "font-weight", label: "fontWeight", group: "style", control: { type: "select", options: WEIGHTS } },
1974
+ { attr: "color", label: "textColor", group: "style", control: { type: "color" }, default: "#000000" },
1975
+ { attr: "line-height", label: "lineHeight", group: "style", control: { type: "length", units: LINE_HEIGHT }, default: "1" },
1976
+ { attr: "container-background-color", label: "blockBackground", group: "style", control: { type: "color" } },
1977
+ { attr: "letter-spacing", label: "letterSpacing", group: "advanced", control: { type: "length", units: LETTER_SPACING, negative: true } },
1978
+ { attr: "text-transform", label: "textTransform", group: "advanced", control: { type: "select", options: TEXT_TRANSFORM } }
1979
+ ],
1980
+ "mj-button": [
1981
+ { attr: "href", label: "link", group: "content", control: { type: "link" } },
1982
+ { attr: "align", label: "align", group: "layout", control: { type: "segmented", options: ALIGN }, default: "center" },
1983
+ { attr: "inner-padding", label: "buttonPadding", group: "spacing", control: { type: "box", units: PX_PCT }, default: "10px 25px" },
1984
+ { attr: "padding", label: "spaceAround", group: "spacing", control: { type: "box", units: PX_PCT }, default: "10px 25px" },
1985
+ { attr: "background-color", label: "buttonColor", group: "style", control: { type: "color" }, default: "#414141" },
1986
+ { attr: "color", label: "textColor", group: "style", control: { type: "color" }, default: "#ffffff" },
1987
+ { attr: "font-size", label: "fontSize", group: "style", control: { type: "length", units: PX }, default: "13px" },
1988
+ { attr: "border-radius", label: "cornerRadius", group: "style", control: { type: "length", units: PX_PCT }, default: "3px" },
1989
+ { attr: "width", label: "width", group: "advanced", control: { type: "length", units: PX_PCT } },
1990
+ { attr: "font-family", label: "font", group: "advanced", control: { type: "select", options: FONTS }, default: "Ubuntu, Helvetica, Arial, sans-serif" },
1991
+ { attr: "font-weight", label: "fontWeight", group: "advanced", control: { type: "select", options: WEIGHTS }, default: "normal" },
1992
+ { attr: "target", label: "opensIn", group: "advanced", control: { type: "select", options: TARGET }, default: "_blank" },
1993
+ { attr: "title", label: "tooltip", group: "advanced", control: { type: "text" } },
1994
+ { attr: "rel", label: "rel", group: "advanced", control: { type: "text", placeholder: "noopener" } },
1995
+ { attr: "container-background-color", label: "blockBackground", group: "advanced", control: { type: "color" } },
1996
+ { attr: "border", label: "border", group: "advanced", control: { type: "text", placeholder: "1px solid #cccccc" }, default: "none" },
1997
+ { attr: "letter-spacing", label: "letterSpacing", group: "advanced", control: { type: "length", units: LETTER_SPACING, negative: true } },
1998
+ { attr: "text-transform", label: "textTransform", group: "advanced", control: { type: "select", options: TEXT_TRANSFORM }, default: "none" },
1999
+ { attr: "line-height", label: "lineHeight", group: "advanced", control: { type: "length", units: LINE_HEIGHT }, default: "120%" }
2000
+ ],
2001
+ "mj-image": [
2002
+ { attr: "src", label: "image", group: "content", control: { type: "image" } },
2003
+ { attr: "alt", label: "altText", group: "content", control: { type: "text" } },
2004
+ { attr: "href", label: "link", group: "content", control: { type: "link" } },
2005
+ { attr: "width", label: "width", group: "layout", control: { type: "length", units: PX } },
2006
+ { attr: "align", label: "align", group: "layout", control: { type: "segmented", options: ALIGN }, default: "center" },
2007
+ { attr: "fluid-on-mobile", label: "fluidOnMobile", group: "layout", control: { type: "toggle", on: "true" } },
2008
+ { attr: "padding", label: "padding", group: "spacing", control: { type: "box", units: PX_PCT }, default: "10px 25px" },
2009
+ { attr: "border-radius", label: "cornerRadius", group: "style", control: { type: "length", units: PX_PCT } },
2010
+ { attr: "target", label: "opensIn", group: "advanced", control: { type: "select", options: TARGET }, default: "_blank" },
2011
+ { attr: "title", label: "tooltip", group: "advanced", control: { type: "text" } },
2012
+ { attr: "rel", label: "rel", group: "advanced", control: { type: "text", placeholder: "noopener" } },
2013
+ { attr: "border", label: "border", group: "advanced", control: { type: "text", placeholder: "1px solid #cccccc" }, default: "0" },
2014
+ { attr: "container-background-color", label: "blockBackground", group: "advanced", control: { type: "color" } }
2015
+ ],
2016
+ "mj-divider": [
2017
+ { attr: "width", label: "width", group: "layout", control: { type: "length", units: PX_PCT }, default: "100%" },
2018
+ { attr: "align", label: "align", group: "layout", control: { type: "segmented", options: ALIGN }, default: "center" },
2019
+ { attr: "padding", label: "padding", group: "spacing", control: { type: "box", units: PX_PCT }, default: "10px 25px" },
2020
+ { attr: "border-color", label: "lineColor", group: "style", control: { type: "color" }, default: "#000000" },
2021
+ { attr: "border-width", label: "thickness", group: "style", control: { type: "length", units: PX }, default: "4px" },
2022
+ {
2023
+ attr: "border-style",
2024
+ label: "lineStyle",
2025
+ group: "style",
2026
+ control: {
2027
+ type: "segmented",
2028
+ options: [
2029
+ { value: "solid", label: "lineSolid" },
2030
+ { value: "dashed", label: "lineDashed" },
2031
+ { value: "dotted", label: "lineDotted" }
2032
+ ]
2033
+ },
2034
+ default: "solid"
2035
+ },
2036
+ { attr: "container-background-color", label: "blockBackground", group: "advanced", control: { type: "color" } }
2037
+ ],
2038
+ "mj-spacer": [
2039
+ { attr: "height", label: "height", group: "layout", control: { type: "length", units: PX }, default: "20px" },
2040
+ { attr: "container-background-color", label: "blockBackground", group: "advanced", control: { type: "color" } }
2041
+ ],
2042
+ "mj-social": [
2043
+ {
2044
+ attr: "mode",
2045
+ label: "socialMode",
2046
+ group: "layout",
2047
+ control: {
2048
+ type: "segmented",
2049
+ options: [
2050
+ { value: "horizontal", label: "modeHorizontal" },
2051
+ { value: "vertical", label: "modeVertical" }
2052
+ ]
2053
+ },
2054
+ default: "horizontal"
2055
+ },
2056
+ { attr: "align", label: "align", group: "layout", control: { type: "segmented", options: ALIGN }, default: "center" },
2057
+ { attr: "padding", label: "padding", group: "spacing", control: { type: "box", units: PX_PCT }, default: "10px 25px" },
2058
+ { attr: "inner-padding", label: "iconSpacing", group: "spacing", control: { type: "box", units: PX_PCT } },
2059
+ { attr: "icon-size", label: "iconSize", group: "style", control: { type: "length", units: PX_PCT }, default: "20px" },
2060
+ // A single value: mj-social's border-radius is not a 4-side shorthand.
2061
+ { attr: "border-radius", label: "cornerRadius", group: "style", control: { type: "length", units: PX_PCT }, default: "3px" },
2062
+ { attr: "font-family", label: "font", group: "advanced", control: { type: "select", options: FONTS }, default: "Ubuntu, Helvetica, Arial, sans-serif" },
2063
+ { attr: "font-size", label: "fontSize", group: "advanced", control: { type: "length", units: PX }, default: "13px" },
2064
+ { attr: "color", label: "textColor", group: "advanced", control: { type: "color" }, default: "#333333" },
2065
+ { attr: "line-height", label: "lineHeight", group: "advanced", control: { type: "length", units: LINE_HEIGHT }, default: "22px" },
2066
+ { attr: "container-background-color", label: "blockBackground", group: "advanced", control: { type: "color" } }
2067
+ ],
2068
+ "mj-social-element": [
2069
+ { attr: "name", label: "network", group: "content", control: { type: "network", networks: NETWORKS } },
2070
+ { attr: "href", label: "link", group: "content", control: { type: "link" } },
2071
+ {
2072
+ attr: "src",
2073
+ label: "customIcon",
2074
+ group: "content",
2075
+ control: { type: "image" },
2076
+ when: (a) => (a.name ?? "").startsWith("custom") || !!a.src
2077
+ },
2078
+ { attr: "alt", label: "altText", group: "content", control: { type: "text" } },
2079
+ { attr: "background-color", label: "iconColor", group: "style", control: { type: "color" } },
2080
+ { attr: "icon-size", label: "iconSize", group: "advanced", control: { type: "length", units: PX_PCT } },
2081
+ { attr: "border-radius", label: "cornerRadius", group: "advanced", control: { type: "length", units: PX }, default: "3px" },
2082
+ { attr: "padding", label: "padding", group: "advanced", control: { type: "box", units: PX_PCT }, default: "4px" },
2083
+ { attr: "icon-position", label: "iconPosition", group: "advanced", control: { type: "segmented", options: [{ value: "left", label: "alignLeft" }, { value: "right", label: "alignRight" }] }, default: "left" },
2084
+ { attr: "title", label: "tooltip", group: "advanced", control: { type: "text" } },
2085
+ { attr: "target", label: "opensIn", group: "advanced", control: { type: "select", options: TARGET }, default: "_blank" },
2086
+ { attr: "rel", label: "rel", group: "advanced", control: { type: "text", placeholder: "noopener" } }
2087
+ ],
2088
+ "mj-hero": [
2089
+ { attr: "background-url", label: "backgroundImage", group: "content", control: { type: "image" } },
2090
+ {
2091
+ attr: "mode",
2092
+ label: "heroMode",
2093
+ group: "content",
2094
+ control: {
2095
+ type: "segmented",
2096
+ options: [
2097
+ { value: "fixed-height", label: "heroFixed" },
2098
+ { value: "fluid-height", label: "heroFluid" }
2099
+ ]
2100
+ },
2101
+ default: "fixed-height"
2102
+ },
2103
+ {
2104
+ attr: "height",
2105
+ label: "height",
2106
+ group: "layout",
2107
+ control: { type: "length", units: PX_PCT },
2108
+ default: "0px",
2109
+ when: (a) => (a.mode ?? "fixed-height") === "fixed-height"
2110
+ },
2111
+ {
2112
+ attr: "background-width",
2113
+ label: "backgroundWidth",
2114
+ group: "layout",
2115
+ control: { type: "length", units: PX_PCT },
2116
+ when: (a) => a.mode === "fluid-height"
2117
+ },
2118
+ {
2119
+ attr: "background-height",
2120
+ label: "backgroundHeight",
2121
+ group: "layout",
2122
+ control: { type: "length", units: PX_PCT },
2123
+ when: (a) => a.mode === "fluid-height"
2124
+ },
2125
+ { attr: "vertical-align", label: "verticalAlign", group: "layout", control: { type: "segmented", options: VERTICAL_ALIGN }, default: "top" },
2126
+ { attr: "background-position", label: "backgroundPosition", group: "layout", control: { type: "select", options: BACKGROUND_POSITION }, default: "center center" },
2127
+ { attr: "padding", label: "padding", group: "spacing", control: { type: "box", units: PX_PCT }, default: "0px" },
2128
+ { attr: "background-color", label: "fallbackColor", group: "style", control: { type: "color" }, default: "#ffffff" },
2129
+ { attr: "border-radius", label: "cornerRadius", group: "style", control: { type: "length", units: PX_PCT } },
2130
+ { attr: "inner-padding", label: "contentPadding", group: "advanced", control: { type: "box", units: PX_PCT } },
2131
+ { attr: "inner-background-color", label: "contentBackground", group: "advanced", control: { type: "color" } }
2132
+ ],
2133
+ "mj-navbar": [
2134
+ { attr: "align", label: "align", group: "layout", control: { type: "segmented", options: ALIGN }, default: "center" },
2135
+ { attr: "hamburger", label: "hamburger", group: "layout", control: { type: "toggle", on: "hamburger" } },
2136
+ { attr: "padding", label: "padding", group: "spacing", control: { type: "box", units: PX_PCT } },
2137
+ { attr: "ico-color", label: "menuIconColor", group: "advanced", control: { type: "color" }, default: "#000000", when: (a) => !!a.hamburger },
2138
+ { attr: "ico-font-size", label: "menuIconSize", group: "advanced", control: { type: "length", units: PX_PCT }, default: "30px", when: (a) => !!a.hamburger },
2139
+ { attr: "ico-align", label: "menuIconAlign", group: "advanced", control: { type: "segmented", options: ALIGN }, default: "center", when: (a) => !!a.hamburger },
2140
+ { attr: "base-url", label: "baseUrl", group: "advanced", control: { type: "link" } }
2141
+ ],
2142
+ "mj-navbar-link": [
2143
+ { attr: "href", label: "link", group: "content", control: { type: "link" } },
2144
+ { attr: "padding", label: "padding", group: "spacing", control: { type: "box", units: PX_PCT }, default: "15px 10px" },
2145
+ { attr: "color", label: "textColor", group: "style", control: { type: "color" }, default: "#000000" },
2146
+ { attr: "font-family", label: "font", group: "style", control: { type: "select", options: FONTS }, default: "Ubuntu, Helvetica, Arial, sans-serif" },
2147
+ { attr: "font-size", label: "fontSize", group: "style", control: { type: "length", units: PX }, default: "13px" },
2148
+ { attr: "font-weight", label: "fontWeight", group: "style", control: { type: "select", options: WEIGHTS }, default: "normal" },
2149
+ { attr: "text-transform", label: "textTransform", group: "style", control: { type: "select", options: TEXT_TRANSFORM }, default: "uppercase" },
2150
+ { attr: "target", label: "opensIn", group: "advanced", control: { type: "select", options: TARGET }, default: "_blank" },
2151
+ { attr: "rel", label: "rel", group: "advanced", control: { type: "text", placeholder: "noopener" } },
2152
+ { attr: "letter-spacing", label: "letterSpacing", group: "advanced", control: { type: "length", units: LETTER_SPACING, negative: true } },
2153
+ { attr: "line-height", label: "lineHeight", group: "advanced", control: { type: "length", units: LINE_HEIGHT }, default: "22px" }
2154
+ ]
2155
+ };
2156
+
2157
+ // src/components/editor/layers.ts
2158
+ var svg = (paths) => `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">${paths}</svg>`;
2159
+ var ICONS = {
2160
+ "mj-wrapper": svg(
2161
+ '<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"/>'
2162
+ ),
2163
+ "mj-section": svg('<rect width="20" height="12" x="2" y="6" rx="2"/>'),
2164
+ "mj-hero": svg('<rect width="18" height="18" x="3" y="3" rx="2"/><path d="M3 9h18"/>'),
2165
+ "mj-group": svg(
2166
+ '<rect width="18" height="18" x="3" y="3" rx="2"/><path d="M9 3v18"/><path d="M15 3v18"/>'
2167
+ ),
2168
+ "mj-column": svg('<rect width="18" height="18" x="3" y="3" rx="2"/><path d="M12 3v18"/>'),
2169
+ "mj-text": svg('<path d="M4 7V4h16v3"/><path d="M9 20h6"/><path d="M12 4v16"/>'),
2170
+ "mj-image": svg(
2171
+ '<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"/>'
2172
+ ),
2173
+ "mj-button": svg('<rect width="20" height="10" x="2" y="7" rx="3"/><path d="M8 12h8"/>'),
2174
+ "mj-divider": svg('<path d="M5 12h14"/>'),
2175
+ "mj-spacer": svg('<path d="M12 3v18"/><path d="m8 17 4 4 4-4"/><path d="m8 7 4-4 4 4"/>'),
2176
+ "mj-social": svg(
2177
+ '<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"/>'
2178
+ ),
2179
+ "mj-social-element": svg(
2180
+ '<circle cx="12" cy="12" r="4"/><path d="M16 8v5a3 3 0 0 0 6 0v-1a10 10 0 1 0-4 8"/>'
2181
+ ),
2182
+ "mj-navbar": svg('<path d="M4 6h16"/><path d="M4 12h16"/><path d="M4 18h16"/>'),
2183
+ "mj-navbar-link": svg(
2184
+ '<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"/>'
2185
+ ),
2186
+ "mj-raw": svg('<path d="m16 18 6-6-6-6"/><path d="m8 6-6 6 6 6"/>'),
2187
+ "mj-table": svg(
2188
+ '<rect width="18" height="18" x="3" y="3" rx="2"/><path d="M3 9h18"/><path d="M3 15h18"/><path d="M12 3v18"/>'
2189
+ )
682
2190
  };
683
2191
  function tagOf(c) {
684
2192
  return String(c.get("tagName") || c.get("type") || "");
685
2193
  }
686
- 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>';
687
- 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>';
688
- function isSection(c) {
689
- return tagOf(c) === "mj-section";
2194
+ function textOf(c) {
2195
+ const template = document.createElement("template");
2196
+ template.innerHTML = c.getInnerHTML();
2197
+ template.content.querySelectorAll("br, p, div, li, tr, h1, h2, h3, h4, h5, h6").forEach((el) => el.after("\n"));
2198
+ const lines = (template.content.textContent ?? "").split("\n");
2199
+ const first = lines.map((l) => l.replace(/\s+/g, " ").trim()).find(Boolean) ?? "";
2200
+ return first.slice(0, 60);
690
2201
  }
691
- function isCommentable(c) {
692
- const t = tagOf(c);
693
- return t === "mj-section" || t in TYPE_LABEL;
2202
+ function escapeHtml(s) {
2203
+ return s.replace(/[&<>"']/g, (ch) => `&#${ch.charCodeAt(0)};`);
694
2204
  }
695
- function classMatch(c, re) {
696
- const cssClass = String(c.getAttributes()["css-class"] ?? "");
697
- return cssClass.match(re)?.[1] ?? null;
2205
+ function layerName(c, labels) {
2206
+ const tag = tagOf(c);
2207
+ const kind = labels.elementNames[tag] ?? tag.replace(/^mj-/, "");
2208
+ const attributes = c.getAttributes();
2209
+ switch (tag) {
2210
+ case "mj-text":
2211
+ case "mj-button":
2212
+ case "mj-navbar-link":
2213
+ return textOf(c) || kind;
2214
+ case "mj-image":
2215
+ return attributes.alt?.trim() || kind;
2216
+ case "mj-social-element": {
2217
+ const { network } = parseNetwork(String(attributes.name ?? ""));
2218
+ const option = NETWORKS.find((o) => o.value === network && network !== "custom");
2219
+ return textOf(c) || option && (labels.inspectorLabels[option.label] ?? option.label) || kind;
2220
+ }
2221
+ case "mj-section":
2222
+ case "mj-wrapper":
2223
+ case "mj-hero": {
2224
+ let words = "";
2225
+ c.onAll((d) => {
2226
+ if (!words && d !== c && ["mj-text", "mj-button"].includes(tagOf(d))) words = textOf(d);
2227
+ });
2228
+ return words || kind;
2229
+ }
2230
+ case "mj-column": {
2231
+ const columns = c.parent()?.components().filter((s) => tagOf(s) === "mj-column") ?? [];
2232
+ return columns.length > 1 ? `${kind} ${columns.indexOf(c) + 1}` : kind;
2233
+ }
2234
+ default:
2235
+ return kind;
2236
+ }
698
2237
  }
2238
+ var CONTENT = /* @__PURE__ */ new Set(["mj-text", "mj-button", "mj-navbar-link", "mj-social-element", "mj-raw"]);
2239
+ function badgeLabel(labels) {
2240
+ return (c) => {
2241
+ const tag = tagOf(c);
2242
+ const icon = ICONS[tag] ? `<div class="gjs-badge__icon">${ICONS[tag]}</div>` : "";
2243
+ const kind = labels.elementNames[tag] ?? tag.replace(/^mj-/, "");
2244
+ return `${icon}<div class="gjs-badge__name">${escapeHtml(kind)}</div>`;
2245
+ };
2246
+ }
2247
+ function setupLayerNames(editor, labels) {
2248
+ const hideContent = (c) => c.onAll((d) => d !== c && d.set("layerable", false, { avoidStore: true }));
2249
+ for (const type of editor.Components.getTypes()) {
2250
+ if (!type.id.startsWith("mj-")) continue;
2251
+ editor.Components.addType(type.id, {
2252
+ model: {
2253
+ // Ours whatever the element says: GrapesJS writes it into the row's markup as it is.
2254
+ getIcon() {
2255
+ return ICONS[type.id] ?? "";
2256
+ },
2257
+ // GrapesJS writes the name into the row's markup unescaped, and it is document text.
2258
+ getName() {
2259
+ return escapeHtml(layerName(this, labels));
2260
+ },
2261
+ ...CONTENT.has(type.id) && {
2262
+ // Editing a text replaces its children, so they are hidden again as they arrive.
2263
+ init() {
2264
+ hideContent(this);
2265
+ this.listenTo(this.components(), "add reset", () => hideContent(this));
2266
+ }
2267
+ }
2268
+ },
2269
+ // Runs the plugin's own `init` as well.
2270
+ extendFn: ["init"]
2271
+ });
2272
+ }
2273
+ let pending = false;
2274
+ editor.on("update component:add component:remove", () => {
2275
+ if (pending) return;
2276
+ pending = true;
2277
+ requestAnimationFrame(() => {
2278
+ pending = false;
2279
+ editor.Layers.getRoot()?.onAll((c) => {
2280
+ const view = c.viewLayer;
2281
+ const el = view?.getInputName?.();
2282
+ const name = layerName(c, labels);
2283
+ if (!el || el.textContent === name) return;
2284
+ el.textContent = name;
2285
+ el.parentElement?.setAttribute("title", name);
2286
+ });
2287
+ });
2288
+ });
2289
+ }
2290
+
2291
+ // src/components/editor/inspector/StructureSection.tsx
2292
+ import { AtSign, ChevronRight, Link2 as Link22, Plus } from "lucide-react";
2293
+
2294
+ // src/components/editor/inspector/structure.ts
2295
+ function tagOf2(c) {
2296
+ return String(c.get("tagName") || c.get("type") || "");
2297
+ }
2298
+ var COLUMN_RATIOS = {
2299
+ 2: [
2300
+ [1, 1],
2301
+ [1, 2],
2302
+ [2, 1]
2303
+ ],
2304
+ 3: [
2305
+ [1, 1, 1],
2306
+ [1, 2, 1]
2307
+ ]
2308
+ };
2309
+ function columnsOf(section) {
2310
+ const children = section.components().models;
2311
+ return children.length > 0 && children.every((c) => tagOf2(c) === "mj-column") ? children : null;
2312
+ }
2313
+ function ratioWidths(ratio) {
2314
+ if (ratio.every((r) => r === ratio[0])) return null;
2315
+ const total = ratio.reduce((a, b) => a + b, 0);
2316
+ return ratio.map((r) => `${Math.round(r / total * 1e4) / 100}%`);
2317
+ }
2318
+ function currentRatio(columns) {
2319
+ const widths = columns.map((c) => String(c.getAttributes().width ?? ""));
2320
+ return COLUMN_RATIOS[columns.length]?.find((ratio) => {
2321
+ const want = ratioWidths(ratio);
2322
+ if (!want) return widths.every((w) => w === "");
2323
+ return widths.every(
2324
+ (w, i) => /%$/.test(w) && Math.abs(parseFloat(w) - parseFloat(want[i])) < 0.5
2325
+ );
2326
+ }) ?? null;
2327
+ }
2328
+ function setRatio(columns, ratio) {
2329
+ const widths = ratioWidths(ratio);
2330
+ columns.forEach((c, i) => {
2331
+ if (widths) c.addAttributes({ width: widths[i] });
2332
+ else c.removeAttributes(["width"]);
2333
+ });
2334
+ }
2335
+ var PROFILE_URLS = {
2336
+ facebook: "https://facebook.com/",
2337
+ instagram: "https://instagram.com/",
2338
+ x: "https://x.com/",
2339
+ linkedin: "https://linkedin.com/",
2340
+ youtube: "https://youtube.com/",
2341
+ pinterest: "https://pinterest.com/",
2342
+ github: "https://github.com/"
2343
+ };
2344
+ function addNetwork(social) {
2345
+ const present = new Set(
2346
+ social.components().map((c) => String(c.getAttributes().name ?? "").replace(/-noshare$/, ""))
2347
+ );
2348
+ const network = Object.keys(PROFILE_URLS).find((n) => !present.has(n)) ?? "facebook";
2349
+ return social.append(
2350
+ `<mj-social-element name="${network}-noshare" href="${PROFILE_URLS[network]}"></mj-social-element>`
2351
+ )[0];
2352
+ }
2353
+ function addNavbarLink(navbar, text) {
2354
+ return navbar.append(`<mj-navbar-link>${escapeHtml2(text)}</mj-navbar-link>`)[0];
2355
+ }
2356
+ function escapeHtml2(s) {
2357
+ return s.replace(/[&<>"']/g, (ch) => `&#${ch.charCodeAt(0)};`);
2358
+ }
2359
+
2360
+ // src/components/editor/inspector/StructureSection.tsx
2361
+ import { jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
2362
+ function tagOf3(c) {
2363
+ return String(c.get("tagName") || c.get("type") || "");
2364
+ }
2365
+ function StructureSection({
2366
+ editor,
2367
+ component,
2368
+ labels,
2369
+ locked
2370
+ }) {
2371
+ switch (tagOf3(component)) {
2372
+ case "mj-section":
2373
+ return /* @__PURE__ */ jsx6(ColumnWidths, { component, labels, locked });
2374
+ case "mj-social":
2375
+ return /* @__PURE__ */ jsx6(
2376
+ Children,
2377
+ {
2378
+ editor,
2379
+ component,
2380
+ labels,
2381
+ locked,
2382
+ title: labels.socialNetworks,
2383
+ add: labels.addNetwork,
2384
+ icon: /* @__PURE__ */ jsx6(AtSign, { className: "size-3.5 shrink-0" }),
2385
+ onAdd: () => editor.select(addNetwork(component))
2386
+ }
2387
+ );
2388
+ case "mj-navbar":
2389
+ return /* @__PURE__ */ jsx6(
2390
+ Children,
2391
+ {
2392
+ editor,
2393
+ component,
2394
+ labels,
2395
+ locked,
2396
+ title: labels.navbarLinks,
2397
+ add: labels.addLink,
2398
+ icon: /* @__PURE__ */ jsx6(Link22, { className: "size-3.5 shrink-0" }),
2399
+ onAdd: () => editor.select(addNavbarLink(component, labels.newLinkText))
2400
+ }
2401
+ );
2402
+ default:
2403
+ return null;
2404
+ }
2405
+ }
2406
+ function Heading({ children }) {
2407
+ return /* @__PURE__ */ jsx6("h3", { className: "px-3 pb-1 text-[10px] font-semibold tracking-wider text-panel-muted-fg/80 uppercase", children });
2408
+ }
2409
+ function ColumnWidths({
2410
+ component,
2411
+ labels,
2412
+ locked
2413
+ }) {
2414
+ const columns = columnsOf(component);
2415
+ const ratios = columns && COLUMN_RATIOS[columns.length];
2416
+ if (!columns || !ratios) return null;
2417
+ const current = currentRatio(columns);
2418
+ return /* @__PURE__ */ jsxs5("section", { className: "border-b border-panel-border/60 py-2", children: [
2419
+ /* @__PURE__ */ jsx6(Heading, { children: labels.columnWidths }),
2420
+ /* @__PURE__ */ jsx6("div", { className: "flex gap-1.5 px-3 py-1", children: ratios.map((ratio) => {
2421
+ const name = labels.columnRatio(ratio.join(":"));
2422
+ const active = current?.join() === ratio.join();
2423
+ return /* @__PURE__ */ jsx6(
2424
+ "button",
2425
+ {
2426
+ type: "button",
2427
+ disabled: locked,
2428
+ "aria-pressed": active,
2429
+ title: name,
2430
+ "aria-label": name,
2431
+ onClick: () => !active && setRatio(columns, ratio),
2432
+ className: cn(
2433
+ "flex h-8 flex-1 items-center gap-0.5 rounded-md border px-1.5 transition-colors",
2434
+ active ? "border-brand bg-brand/15" : "border-panel-border bg-panel-elevated hover:border-panel-muted-fg"
2435
+ ),
2436
+ children: ratio.map((part, i) => /* @__PURE__ */ jsx6(
2437
+ "span",
2438
+ {
2439
+ style: { flexGrow: part },
2440
+ className: cn(
2441
+ "h-4 basis-0 rounded-sm",
2442
+ active ? "bg-brand" : "bg-panel-muted-fg/50"
2443
+ )
2444
+ },
2445
+ i
2446
+ ))
2447
+ },
2448
+ ratio.join()
2449
+ );
2450
+ }) })
2451
+ ] });
2452
+ }
2453
+ function Children({
2454
+ editor,
2455
+ component,
2456
+ labels,
2457
+ locked,
2458
+ title,
2459
+ add,
2460
+ icon,
2461
+ onAdd
2462
+ }) {
2463
+ const children = component.components().models;
2464
+ return /* @__PURE__ */ jsxs5("section", { className: "border-b border-panel-border/60 py-2", children: [
2465
+ /* @__PURE__ */ jsx6(Heading, { children: title }),
2466
+ /* @__PURE__ */ jsx6("ul", { className: "flex flex-col px-2", children: children.map((child) => /* @__PURE__ */ jsx6("li", { children: /* @__PURE__ */ jsxs5(
2467
+ "button",
2468
+ {
2469
+ type: "button",
2470
+ onClick: () => editor.select(child),
2471
+ 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",
2472
+ children: [
2473
+ /* @__PURE__ */ jsx6("span", { className: "text-panel-muted-fg", children: icon }),
2474
+ /* @__PURE__ */ jsx6("span", { className: "min-w-0 flex-1 truncate", children: layerName(child, labels) }),
2475
+ /* @__PURE__ */ jsx6(ChevronRight, { className: "size-3 shrink-0 text-panel-muted-fg opacity-0 group-hover:opacity-100" })
2476
+ ]
2477
+ }
2478
+ ) }, child.cid)) }),
2479
+ /* @__PURE__ */ jsxs5(
2480
+ "button",
2481
+ {
2482
+ type: "button",
2483
+ disabled: locked,
2484
+ onClick: onAdd,
2485
+ 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",
2486
+ children: [
2487
+ /* @__PURE__ */ jsx6(Plus, { className: "size-3.5" }),
2488
+ add
2489
+ ]
2490
+ }
2491
+ )
2492
+ ] });
2493
+ }
2494
+
2495
+ // src/components/editor/inspector/write.ts
2496
+ function same(a, b) {
2497
+ const keys = Object.keys(a);
2498
+ return keys.length === Object.keys(b).length && keys.every((k) => k in b && a[k] === b[k]);
2499
+ }
2500
+ function createWriter(editor, component, blocked = () => false) {
2501
+ const isImage = String(component.get("tagName") || component.get("type")) === "mj-image";
2502
+ const current = () => {
2503
+ const { __p: _marker, style: _style, ...rest } = component.get("attributes");
2504
+ return rest;
2505
+ };
2506
+ let drag = null;
2507
+ const record = (start, end) => {
2508
+ component.setAttributes(start, { avoidStore: true });
2509
+ if (!same(end, start)) component.setAttributes(end);
2510
+ delete component.get("attributes").__p;
2511
+ };
2512
+ return async (attr, value, { partial, remove = [] } = {}) => {
2513
+ if (editor.getEditing() === component) {
2514
+ const view = component.getView();
2515
+ await view?.syncContent?.();
2516
+ }
2517
+ if (blocked()) return;
2518
+ if (drag && drag.attr !== attr) {
2519
+ const { start: start2 } = drag;
2520
+ drag = null;
2521
+ record(start2, current());
2522
+ }
2523
+ if (attr === "src" && isImage) return void component.set({ src: value });
2524
+ const next = current();
2525
+ for (const name of remove) delete next[name];
2526
+ if (value === "") delete next[attr];
2527
+ else next[attr] = value;
2528
+ if (partial) {
2529
+ drag ??= { attr, start: current() };
2530
+ component.setAttributes(next, { avoidStore: true });
2531
+ return;
2532
+ }
2533
+ const start = drag?.start ?? current();
2534
+ drag = null;
2535
+ record(start, next);
2536
+ };
2537
+ }
2538
+
2539
+ // src/components/editor/inspector/Inspector.tsx
2540
+ import { Fragment, jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
2541
+ var GROUPS = ["content", "layout", "spacing", "style"];
2542
+ var HIDDEN = /* @__PURE__ */ new Set(["css-class", "mj-class", "id", "style"]);
2543
+ function tagOf4(c) {
2544
+ return String(c.get("tagName") || c.get("type") || "");
2545
+ }
2546
+ function attributesOf(c) {
2547
+ const out = {};
2548
+ for (const [k, v] of Object.entries(c.getAttributes())) {
2549
+ if (v !== void 0 && v !== null && v !== false) out[k] = String(v);
2550
+ }
2551
+ if (tagOf4(c) === "mj-image") out.src = String(c.get("src") ?? "");
2552
+ return out;
2553
+ }
2554
+ function documentColors(editor) {
2555
+ const counts = /* @__PURE__ */ new Map();
2556
+ const colorValue = /(?:color|background|border)[\w-]*\s*[:=]\s*["']?[^"';>{}]*?(#[0-9a-f]{3,8}\b|rgba?\([^)]*\))/gi;
2557
+ for (const match of editor.getHtml().matchAll(colorValue)) {
2558
+ const hex = toHex(match[1], { opaque: true });
2559
+ if (hex) counts.set(hex, (counts.get(hex) ?? 0) + 1);
2560
+ }
2561
+ return [...counts.entries()].sort((a, b) => b[1] - a[1]).slice(0, 14).map(([hex]) => hex);
2562
+ }
2563
+ function Inspector({
2564
+ onEmailSettings,
2565
+ empty
2566
+ }) {
2567
+ const editor = useEditorMaybe();
2568
+ const labels = useLabels();
2569
+ const { locked } = useAgentTurn();
2570
+ const lockedNow = useRef4(locked);
2571
+ useLayoutEffect(() => {
2572
+ lockedNow.current = locked;
2573
+ });
2574
+ const [, refresh] = useReducer((n) => n + 1, 0);
2575
+ const root = useRef4(null);
2576
+ const refocus = useRef4(false);
2577
+ useEffect4(() => {
2578
+ if (!editor) return;
2579
+ const events = "component:update change:device";
2580
+ const toggled = () => {
2581
+ if (root.current?.contains(document.activeElement)) refocus.current = true;
2582
+ refresh();
2583
+ };
2584
+ editor.on(events, refresh);
2585
+ editor.on("component:toggled", toggled);
2586
+ return () => {
2587
+ editor.off(events, refresh);
2588
+ editor.off("component:toggled", toggled);
2589
+ };
2590
+ }, [editor]);
2591
+ useEffect4(() => {
2592
+ if (!refocus.current) return;
2593
+ refocus.current = false;
2594
+ root.current?.querySelector("[data-focus-target]")?.focus();
2595
+ });
2596
+ if (!editor) return null;
2597
+ const selected = editor.getSelectedAll();
2598
+ const component = selected[0];
2599
+ return /* @__PURE__ */ jsx7("div", { ref: root, children: selected.length === 0 ? /* @__PURE__ */ jsx7("div", { "data-focus-target": true, tabIndex: -1, className: "outline-none", children: empty }) : selected.length > 1 ? /* @__PURE__ */ jsx7(
2600
+ "p",
2601
+ {
2602
+ "data-focus-target": true,
2603
+ tabIndex: -1,
2604
+ className: "px-4 py-8 text-center text-xs text-panel-muted-fg outline-none",
2605
+ children: labels.multipleSelected(selected.length)
2606
+ }
2607
+ ) : /* @__PURE__ */ jsx7(
2608
+ ElementSettings,
2609
+ {
2610
+ editor,
2611
+ component,
2612
+ labels,
2613
+ locked,
2614
+ lockedNow,
2615
+ onEmailSettings
2616
+ },
2617
+ component.cid
2618
+ ) });
2619
+ }
2620
+ function ElementSettings({
2621
+ editor,
2622
+ component,
2623
+ labels,
2624
+ locked,
2625
+ lockedNow,
2626
+ onEmailSettings
2627
+ }) {
2628
+ const [advancedOpen, setAdvancedOpen] = useState4(false);
2629
+ const tag = tagOf4(component);
2630
+ const fields = SCHEMA[tag] ?? [];
2631
+ const attributes = attributesOf(component);
2632
+ const visible = fields.filter((f) => !f.when || f.when(attributes));
2633
+ const t = (key) => labels.inspectorLabels[key] ?? key;
2634
+ const optionName = (o) => labels.inspectorLabels[o.label] ?? o.label;
2635
+ const [writer] = useState4(() => createWriter(editor, component, () => lockedNow.current));
2636
+ const write = (attr, value, options) => lockedNow.current ? Promise.resolve() : writer(attr, value, options);
2637
+ const writeBox = (attr, shorthand, partial) => write(attr, shorthand, { remove: SIDES.map((s) => `${attr}-${s}`), partial });
2638
+ const chooseImage = (attr) => {
2639
+ if (attr === "src" && tag === "mj-image") return void editor.runCommand("open-image-picker");
2640
+ const am = editor.AssetManager;
2641
+ am.open({
2642
+ types: ["image"],
2643
+ current: imageUrl(attributes[attr] ?? "") ?? void 0,
2644
+ select(asset, complete) {
2645
+ void write(attr, typeof asset === "string" ? asset : asset.getSrc());
2646
+ if (complete) am.close();
2647
+ }
2648
+ });
2649
+ };
2650
+ const safeLink = (attr) => (link) => {
2651
+ if (tag === "mj-navbar-link" && attr === "href") {
2652
+ return isSafeNavbarLink(String(component.parent()?.getAttributes()["base-url"] ?? ""), link);
2653
+ }
2654
+ if (tag === "mj-navbar" && attr === "base-url") {
2655
+ return component.components().every((c) => isSafeNavbarLink(link, String(c.getAttributes().href ?? "")));
2656
+ }
2657
+ return isSafeLink(link);
2658
+ };
2659
+ const control = (f) => {
2660
+ const value = attributes[f.attr] ?? "";
2661
+ const c = f.control;
2662
+ switch (c.type) {
2663
+ case "text":
2664
+ return /* @__PURE__ */ jsx7(
2665
+ TextInput,
2666
+ {
2667
+ value,
2668
+ placeholder: c.placeholder ?? f.default,
2669
+ onCommit: (v) => write(f.attr, v)
2670
+ }
2671
+ );
2672
+ case "link":
2673
+ return /* @__PURE__ */ jsx7(
2674
+ TextInput,
2675
+ {
2676
+ value,
2677
+ placeholder: "https://",
2678
+ accept: safeLink(f.attr),
2679
+ onCommit: (v) => write(f.attr, v)
2680
+ }
2681
+ );
2682
+ case "color":
2683
+ return /* @__PURE__ */ jsx7(
2684
+ ColorField,
2685
+ {
2686
+ value,
2687
+ placeholder: f.default,
2688
+ documentColors: () => documentColors(editor),
2689
+ labels,
2690
+ onChange: (v, partial) => write(f.attr, v, { partial })
2691
+ }
2692
+ );
2693
+ case "image":
2694
+ return /* @__PURE__ */ jsx7(
2695
+ ImageField,
2696
+ {
2697
+ value,
2698
+ labels,
2699
+ onChoose: () => chooseImage(f.attr),
2700
+ onClear: () => write(f.attr, "")
2701
+ }
2702
+ );
2703
+ case "length":
2704
+ return /* @__PURE__ */ jsx7(
2705
+ LengthInput,
2706
+ {
2707
+ value,
2708
+ placeholder: f.default,
2709
+ units: c.units,
2710
+ negative: c.negative,
2711
+ labels,
2712
+ onCommit: (v, partial) => write(f.attr, v, { partial })
2713
+ }
2714
+ );
2715
+ case "box":
2716
+ return /* @__PURE__ */ jsx7(
2717
+ BoxInput,
2718
+ {
2719
+ attributes,
2720
+ attr: f.attr,
2721
+ placeholder: f.default,
2722
+ units: c.units,
2723
+ labels,
2724
+ onCommit: (v, partial) => writeBox(f.attr, v, partial)
2725
+ }
2726
+ );
2727
+ case "segmented":
2728
+ return /* @__PURE__ */ jsx7(
2729
+ Segmented,
2730
+ {
2731
+ value,
2732
+ fallback: f.default,
2733
+ options: c.options,
2734
+ name: optionName,
2735
+ onCommit: (v) => write(f.attr, v)
2736
+ }
2737
+ );
2738
+ case "select":
2739
+ return /* @__PURE__ */ jsx7(
2740
+ Select,
2741
+ {
2742
+ value,
2743
+ fallback: f.default,
2744
+ options: c.options,
2745
+ name: optionName,
2746
+ onCommit: (v) => write(f.attr, v)
2747
+ }
2748
+ );
2749
+ case "toggle":
2750
+ return /* @__PURE__ */ jsx7(
2751
+ Toggle,
2752
+ {
2753
+ checked: value === c.on,
2754
+ label: t(f.label),
2755
+ onCommit: (on) => write(f.attr, on ? c.on : "")
2756
+ }
2757
+ );
2758
+ case "network": {
2759
+ const { network, share } = parseNetwork(value);
2760
+ return /* @__PURE__ */ jsxs6("div", { className: "flex flex-col gap-1.5", children: [
2761
+ /* @__PURE__ */ jsx7(
2762
+ Select,
2763
+ {
2764
+ value: network,
2765
+ options: c.networks,
2766
+ name: optionName,
2767
+ onCommit: (n) => write(f.attr, n ? networkName(n, share) : "")
2768
+ }
2769
+ ),
2770
+ network && network !== "custom" && /* @__PURE__ */ jsx7(
2771
+ Segmented,
2772
+ {
2773
+ value: share ? "share" : "profile",
2774
+ options: [
2775
+ { value: "profile", label: labels.linkTypeProfile },
2776
+ { value: "share", label: labels.linkTypeShare }
2777
+ ],
2778
+ name: (o) => o.label,
2779
+ onCommit: (v) => write(f.attr, networkName(network, v === "share"))
2780
+ }
2781
+ )
2782
+ ] });
2783
+ }
2784
+ }
2785
+ };
2786
+ const own = (f) => f.control.type === "box" ? [f.attr, ...SIDES.map((s) => `${f.attr}-${s}`)].some((a) => attributes[a] !== void 0) : !!attributes[f.attr];
2787
+ const reset = (f) => f.control.type === "box" ? writeBox(f.attr, "") : write(f.attr, "");
2788
+ const wide = (f) => ["color", "image", "link", "text", "box", "network"].includes(f.control.type);
2789
+ const row = (f) => /* @__PURE__ */ jsx7(
2790
+ Row,
2791
+ {
2792
+ label: t(f.label),
2793
+ help: labels.inspectorLabels[`${f.label}.help`],
2794
+ own: own(f),
2795
+ wide: wide(f),
2796
+ inline: f.control.type === "toggle",
2797
+ labels,
2798
+ onReset: () => void reset(f),
2799
+ children: control(f)
2800
+ },
2801
+ f.attr
2802
+ );
2803
+ const named = new Set(
2804
+ fields.flatMap(
2805
+ (f) => f.control.type === "box" ? [f.attr, ...SIDES.map((s) => `${f.attr}-${s}`)] : [f.attr]
2806
+ )
2807
+ );
2808
+ const others = Object.entries(attributes).filter(([k]) => !named.has(k) && !HIDDEN.has(k));
2809
+ const advanced = visible.filter((f) => f.group === "advanced");
2810
+ const advancedCount = advanced.length + others.length;
2811
+ return /* @__PURE__ */ jsxs6("div", { className: "pb-6", children: [
2812
+ /* @__PURE__ */ jsx7(
2813
+ Header,
2814
+ {
2815
+ editor,
2816
+ component,
2817
+ labels,
2818
+ onEmailSettings
2819
+ }
2820
+ ),
2821
+ editor.getDevice() !== "Desktop" && /* @__PURE__ */ jsxs6("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: [
2822
+ /* @__PURE__ */ jsx7(Monitor, { className: "size-3.5 shrink-0" }),
2823
+ labels.allScreenSizes
2824
+ ] }),
2825
+ /* @__PURE__ */ jsx7(StructureSection, { editor, component, labels, locked }),
2826
+ GROUPS.map((group) => {
2827
+ const inGroup = visible.filter((f) => f.group === group);
2828
+ if (inGroup.length === 0) return null;
2829
+ return /* @__PURE__ */ jsxs6("section", { className: "border-b border-panel-border/60 py-2", children: [
2830
+ /* @__PURE__ */ jsx7("h3", { className: "px-3 pb-1 text-[10px] font-semibold tracking-wider text-panel-muted-fg/80 uppercase", children: groupName(group, labels) }),
2831
+ inGroup.map(row)
2832
+ ] }, group);
2833
+ }),
2834
+ advancedCount > 0 && /* @__PURE__ */ jsxs6("section", { className: "py-1", children: [
2835
+ /* @__PURE__ */ jsxs6(
2836
+ "button",
2837
+ {
2838
+ type: "button",
2839
+ onClick: () => setAdvancedOpen((o) => !o),
2840
+ "aria-expanded": advancedOpen,
2841
+ 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",
2842
+ children: [
2843
+ /* @__PURE__ */ jsx7(
2844
+ ChevronRight2,
2845
+ {
2846
+ className: cn("size-3 transition-transform", advancedOpen && "rotate-90")
2847
+ }
2848
+ ),
2849
+ labels.groupAdvanced,
2850
+ /* @__PURE__ */ jsxs6("span", { className: "font-normal normal-case", children: [
2851
+ "(",
2852
+ advancedCount,
2853
+ ")"
2854
+ ] })
2855
+ ]
2856
+ }
2857
+ ),
2858
+ advancedOpen && /* @__PURE__ */ jsxs6(Fragment, { children: [
2859
+ advanced.map(row),
2860
+ others.length > 0 && /* @__PURE__ */ jsxs6("div", { className: "px-3 pt-2", children: [
2861
+ /* @__PURE__ */ jsx7("p", { className: "pb-1 text-[11px] text-panel-muted-fg", children: labels.otherAttributes }),
2862
+ /* @__PURE__ */ jsx7("ul", { className: "flex flex-col gap-1", children: others.map(([k, v]) => /* @__PURE__ */ jsxs6(
2863
+ "li",
2864
+ {
2865
+ className: "flex items-center gap-2 rounded-md bg-panel-elevated px-2 py-1 font-mono text-[11px]",
2866
+ children: [
2867
+ /* @__PURE__ */ jsx7("span", { className: "text-panel-muted-fg", children: k }),
2868
+ /* @__PURE__ */ jsx7("span", { className: "min-w-0 flex-1 truncate text-panel-fg", title: v, children: v }),
2869
+ /* @__PURE__ */ jsx7(
2870
+ "button",
2871
+ {
2872
+ type: "button",
2873
+ onClick: () => void write(k, ""),
2874
+ title: labels.removeAttribute,
2875
+ "aria-label": `${labels.removeAttribute}: ${k}`,
2876
+ className: "text-panel-muted-fg hover:text-panel-fg",
2877
+ children: /* @__PURE__ */ jsx7(X2, { className: "size-3" })
2878
+ }
2879
+ )
2880
+ ]
2881
+ },
2882
+ k
2883
+ )) })
2884
+ ] })
2885
+ ] })
2886
+ ] })
2887
+ ] });
2888
+ }
2889
+ function groupName(group, labels) {
2890
+ return {
2891
+ content: labels.groupContent,
2892
+ layout: labels.groupLayout,
2893
+ spacing: labels.groupSpacing,
2894
+ style: labels.groupStyle
2895
+ }[group];
2896
+ }
2897
+ function Header({
2898
+ editor,
2899
+ component,
2900
+ labels,
2901
+ onEmailSettings
2902
+ }) {
2903
+ const tag = tagOf4(component);
2904
+ const isBody = tag === "mj-body";
2905
+ const name = isBody ? labels.emailSettings : labels.elementNames[tag] ?? tag.replace(/^mj-/, "");
2906
+ const words = tag === "mj-column" ? "" : layerName(component, labels);
2907
+ const excerpt = words === name ? "" : words;
2908
+ const parent = component.parent();
2909
+ const action = "flex size-7 items-center justify-center rounded-md text-panel-muted-fg hover:bg-panel-hover hover:text-panel-fg";
2910
+ return /* @__PURE__ */ jsxs6("div", { className: "sticky top-0 z-10 flex items-center gap-2 border-b border-panel-border bg-panel px-3 py-2", children: [
2911
+ /* @__PURE__ */ jsxs6("div", { className: "min-w-0 flex-1", children: [
2912
+ /* @__PURE__ */ jsx7(
2913
+ "p",
2914
+ {
2915
+ "data-focus-target": true,
2916
+ tabIndex: -1,
2917
+ className: "truncate text-sm font-medium text-panel-fg outline-none",
2918
+ children: name
2919
+ }
2920
+ ),
2921
+ excerpt && !isBody && /* @__PURE__ */ jsxs6("p", { className: "truncate text-[11px] text-panel-muted-fg", children: [
2922
+ "\u201C",
2923
+ excerpt,
2924
+ "\u201D"
2925
+ ] })
2926
+ ] }),
2927
+ !isBody && parent && /* @__PURE__ */ jsx7(
2928
+ "button",
2929
+ {
2930
+ type: "button",
2931
+ className: action,
2932
+ title: labels.selectParent,
2933
+ "aria-label": labels.selectParent,
2934
+ onClick: () => tagOf4(parent) === "mj-body" ? onEmailSettings() : editor.select(parent),
2935
+ children: /* @__PURE__ */ jsx7(ArrowUp, { className: "size-3.5" })
2936
+ }
2937
+ ),
2938
+ !isBody && /* @__PURE__ */ jsxs6(Fragment, { children: [
2939
+ /* @__PURE__ */ jsx7(
2940
+ "button",
2941
+ {
2942
+ type: "button",
2943
+ className: action,
2944
+ title: labels.duplicate,
2945
+ "aria-label": labels.duplicate,
2946
+ onClick: () => editor.runCommand("tlb-clone"),
2947
+ children: /* @__PURE__ */ jsx7(Copy, { className: "size-3.5" })
2948
+ }
2949
+ ),
2950
+ /* @__PURE__ */ jsx7(
2951
+ "button",
2952
+ {
2953
+ type: "button",
2954
+ className: action,
2955
+ title: labels.deleteElement,
2956
+ "aria-label": labels.deleteElement,
2957
+ onClick: () => editor.runCommand("tlb-delete"),
2958
+ children: /* @__PURE__ */ jsx7(Trash22, { className: "size-3.5" })
2959
+ }
2960
+ )
2961
+ ] })
2962
+ ] });
2963
+ }
2964
+
2965
+ // src/components/editor/blocks.ts
2966
+ import grapesjs from "grapesjs";
2967
+ import grapesjsMJML from "grapesjs-mjml";
2968
+
2969
+ // src/components/editor/ids.ts
2970
+ var ID_RE = {
2971
+ sec: /(?:^|\s)sec-([A-Za-z0-9_-]+)(?=\s|$)/,
2972
+ obj: /(?:^|\s)obj-([A-Za-z0-9_-]+)(?=\s|$)/
2973
+ };
2974
+ function idOf(c, prefix) {
2975
+ return String(c.getAttributes()["css-class"] ?? "").match(ID_RE[prefix])?.[1] ?? null;
2976
+ }
2977
+ var newId = () => Math.random().toString(36).slice(2, 10);
699
2978
  function sectionIdOf(c) {
700
- return classMatch(c, SEC_ID_RE);
2979
+ return idOf(c, "sec");
701
2980
  }
702
2981
  function addIdClass(c, prefix) {
703
- const existing = classMatch(c, prefix === "sec" ? SEC_ID_RE : OBJ_ID_RE);
2982
+ const existing = idOf(c, prefix);
704
2983
  if (existing) return existing;
705
2984
  const cssClass = String(c.getAttributes()["css-class"] ?? "");
706
- const id = Math.random().toString(36).slice(2, 10);
2985
+ const id = newId();
707
2986
  c.addAttributes({ "css-class": `${cssClass} ${prefix}-${id}`.trim() });
708
2987
  return id;
709
2988
  }
2989
+ function freshenCopiedIds(root, added) {
2990
+ const used = (prefix, id, except) => {
2991
+ let found = false;
2992
+ root?.onAll((o) => {
2993
+ if (o !== except && idOf(o, prefix) === id) found = true;
2994
+ });
2995
+ return found;
2996
+ };
2997
+ added.onAll((c) => {
2998
+ for (const prefix of ["sec", "obj"]) {
2999
+ const id = idOf(c, prefix);
3000
+ if (!id || !used(prefix, id, c)) continue;
3001
+ const cssClass = String(c.getAttributes()["css-class"]);
3002
+ const fresh = cssClass.replace(
3003
+ ID_RE[prefix],
3004
+ (token) => token.replace(`${prefix}-${id}`, `${prefix}-${newId()}`)
3005
+ );
3006
+ c.addAttributes({ "css-class": fresh });
3007
+ }
3008
+ });
3009
+ }
3010
+
3011
+ // src/components/editor/insert-block.ts
3012
+ var addsBlock = (e) => (e.key === "Enter" || e.key === " ") && !e.repeat && !e.ctrlKey && !e.metaKey && !e.altKey;
3013
+ var tileAttributes = (id) => ({ tabindex: "0", role: "button", "data-block": id });
3014
+ var tagOf5 = (c) => String(c.get("tagName") ?? "");
3015
+ function closest(c, tag) {
3016
+ for (let cur = c; cur; cur = cur.parent() ?? void 0) if (tagOf5(cur) === tag) return cur;
3017
+ return void 0;
3018
+ }
3019
+ function childOf(parent, c) {
3020
+ let cur = c;
3021
+ while (cur.parent() && cur.parent() !== parent) cur = cur.parent();
3022
+ return cur;
3023
+ }
3024
+ function insertBlock(editor, block) {
3025
+ const content = block.getContent();
3026
+ let body;
3027
+ editor.getWrapper()?.onAll((c) => {
3028
+ if (!body && tagOf5(c) === "mj-body") body = c;
3029
+ });
3030
+ if (typeof content !== "string" || !body) return void 0;
3031
+ const selected = editor.getSelected();
3032
+ const section = closest(selected, "mj-section");
3033
+ const sections = section?.parent() ?? body;
3034
+ const afterSection = section ? { at: section.index() + 1 } : {};
3035
+ const column = closest(selected, "mj-column");
3036
+ let added;
3037
+ if (/^\s*<mj-section\b/i.test(content)) {
3038
+ added = sections.append(content, afterSection)[0];
3039
+ } else if (column && selected) {
3040
+ const at = selected === column ? {} : { at: childOf(column, selected).index() + 1 };
3041
+ added = column.append(content, at)[0];
3042
+ } else {
3043
+ const [wrapper] = sections.append(
3044
+ `<mj-section><mj-column>${content}</mj-column></mj-section>`,
3045
+ afterSection
3046
+ );
3047
+ added = wrapper?.components().at(0)?.components().at(0);
3048
+ }
3049
+ if (!added) return void 0;
3050
+ editor.select(added);
3051
+ if (block.get("activate")) added.getView()?.onActive(void 0);
3052
+ return added;
3053
+ }
3054
+
3055
+ // src/components/editor/blocks.ts
3056
+ var EDITABLE_TAGS = /* @__PURE__ */ new Set([
3057
+ "mj-section",
3058
+ "mj-group",
3059
+ "mj-column",
3060
+ "mj-text",
3061
+ "mj-image",
3062
+ "mj-button",
3063
+ "mj-divider",
3064
+ "mj-spacer",
3065
+ "mj-social",
3066
+ "mj-social-element",
3067
+ "mj-navbar",
3068
+ "mj-navbar-link"
3069
+ ]);
3070
+ var CONTENT_TAGS = /* @__PURE__ */ new Set(["mj-text", "mj-button", "mj-social-element", "mj-navbar-link"]);
3071
+ var EDITOR_INTERNALS = /data-gjs-|__p\b|custom-name/i;
3072
+ var DROPPED_MARKUP = /<(?:script|style|link|meta|iframe|object|embed|form)\b/i;
3073
+ var INLINE_IMAGE = /\b(?:src|srcset|background-url|icon)\s*=\s*(?:"[^"]*data:|'[^']*data:|data:)/i;
3074
+ var LINK_IN_MARKUP = /[\s/"'](?:xlink:)?(?:href|action|formaction)\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s>]+))/gi;
3075
+ var validator;
3076
+ function blockProblems(mjml, editor = validator ??= grapesjs.init({
3077
+ headless: true,
3078
+ storageManager: false,
3079
+ telemetry: false,
3080
+ plugins: [grapesjsMJML]
3081
+ })) {
3082
+ if (EDITOR_INTERNALS.test(mjml)) return ["it contains GrapesJS's own attributes"];
3083
+ if (INLINE_IMAGE.test(mjml)) return ["it inlines an image as a data: URL"];
3084
+ let tree;
3085
+ try {
3086
+ tree = editor.runCommand("mjml-code-to-html", {
3087
+ mjml: `<mjml><mj-body>${withClosedTags(mjml)}</mj-body></mjml>`,
3088
+ validationLevel: "strict"
3089
+ }).json;
3090
+ } catch (e) {
3091
+ const errors = e.errors;
3092
+ if (!errors) throw e;
3093
+ return errors.map((error) => error.formattedMessage ?? error.message ?? String(error));
3094
+ }
3095
+ const problems = [];
3096
+ const top = tree.children?.find((c) => c.tagName === "mj-body")?.children ?? [];
3097
+ if (top.length !== 1 || top[0].tagName !== "mj-section") {
3098
+ problems.push("it is not exactly one mj-section");
3099
+ }
3100
+ const visit = (node, parent) => {
3101
+ const tag = node.tagName;
3102
+ const attributes = node.attributes ?? {};
3103
+ const content = node.content ?? "";
3104
+ if (!EDITABLE_TAGS.has(tag)) problems.push(`${tag} is not editable here`);
3105
+ const cssClass = attributes["css-class"] ?? "";
3106
+ if (ID_RE.sec.test(cssClass) || ID_RE.obj.test(cssClass)) {
3107
+ problems.push(`${tag} carries a sec-* or obj-* id`);
3108
+ }
3109
+ if ("mj-class" in attributes) problems.push(`${tag} uses mj-class`);
3110
+ const links = [attributes.href ?? ""];
3111
+ for (const match of content.matchAll(LINK_IN_MARKUP)) {
3112
+ links.push(match[1] ?? match[2] ?? match[3] ?? "");
3113
+ }
3114
+ const linked = tag === "mj-navbar-link" ? isSafeNavbarLink(parent?.attributes?.["base-url"] ?? "", attributes.href ?? "") : true;
3115
+ if (!linked || !links.every(isSafeLink)) {
3116
+ problems.push(`${tag} has a link scheme mail must not carry`);
3117
+ }
3118
+ if (CONTENT_TAGS.has(tag) ? /<mj-/i.test(content) || DROPPED_MARKUP.test(content) : content.trim()) {
3119
+ problems.push(`${tag} holds content the editor does not keep`);
3120
+ }
3121
+ node.children?.forEach((child) => visit(child, node));
3122
+ };
3123
+ top.forEach((node) => visit(node));
3124
+ return problems;
3125
+ }
3126
+ var escapeHtml3 = (text) => text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
3127
+ function registerBlocks(editor, blocks, category, media) {
3128
+ for (const block of blocks) {
3129
+ const problems = editor.Blocks.get(block.id) ? [`the id is already taken`] : blockProblems(block.mjml, editor);
3130
+ if (problems.length > 0) {
3131
+ console.error(`Block "${block.id}" is left out of the palette: ${problems.join("; ")}`);
3132
+ continue;
3133
+ }
3134
+ editor.Blocks.add(block.id, {
3135
+ // GrapesJS writes the label into the tile as markup.
3136
+ label: escapeHtml3(block.label),
3137
+ category,
3138
+ media: block.media ?? media,
3139
+ content: withClosedTags(block.mjml),
3140
+ attributes: tileAttributes(block.id)
3141
+ });
3142
+ }
3143
+ }
3144
+
3145
+ // src/components/editor/builtin-blocks.ts
3146
+ var BUILTIN_BLOCKS = [
3147
+ "mj-1-column",
3148
+ "mj-2-columns",
3149
+ "mj-3-columns",
3150
+ "mj-text",
3151
+ "mj-image",
3152
+ "mj-button",
3153
+ "mj-divider",
3154
+ "mj-spacer",
3155
+ "mj-social-group",
3156
+ "mj-navbar"
3157
+ ];
3158
+ var LAYOUT_BLOCKS = /* @__PURE__ */ new Set(["mj-1-column", "mj-2-columns", "mj-3-columns"]);
3159
+ var CONTENT2 = {
3160
+ // The plugin's version links Google+ and shares the page: a bare network name makes the
3161
+ // icon open a "share this link" dialog, while `-noshare` links to the profile a footer
3162
+ // means. Without `alt` each link is an unnamed image to a screen reader.
3163
+ "mj-social-group": `<mj-social icon-size="24px" mode="horizontal">
3164
+ <mj-social-element name="facebook-noshare" href="https://facebook.com/" alt="Facebook"></mj-social-element>
3165
+ <mj-social-element name="instagram-noshare" href="https://instagram.com/" alt="Instagram"></mj-social-element>
3166
+ <mj-social-element name="x-noshare" href="https://x.com/" alt="X"></mj-social-element>
3167
+ </mj-social>`,
3168
+ // The plugin's sample is a via.placeholder.com picture, stored in the email and loaded by
3169
+ // every reader's mail client. Dropping one opens the image picker instead.
3170
+ "mj-image": "<mj-image></mj-image>"
3171
+ };
3172
+ function builtinBlock(labels) {
3173
+ return (id) => ({
3174
+ label: labels.blockLabels[id] ?? id,
3175
+ // By id: a regex on the label broke as soon as a host translated it.
3176
+ category: LAYOUT_BLOCKS.has(id) ? labels.blockCategoryLayout : labels.blockCategoryContent,
3177
+ ...id in CONTENT2 && { content: CONTENT2[id] },
3178
+ attributes: tileAttributes(id)
3179
+ });
3180
+ }
3181
+ var CANVAS_CSS = 'img:not([src]), img[src=""] { min-height: 120px; background: #f4f4f5; outline: 1px dashed #a1a1aa; outline-offset: -1px; }';
3182
+
3183
+ // src/components/editor/table-type.ts
3184
+ import { isSafeLink as isSafeLink2 } from "@mjml-agent-editor/core";
3185
+ var DROPPED = "script, style, link, iframe, frame, object, embed, applet, base, meta, animate, set, svg, math, template";
3186
+ var LINK_ATTRIBUTES = /* @__PURE__ */ new Set(["href", "xlink:href", "action", "formaction"]);
3187
+ function cleanRows(rows) {
3188
+ rows.querySelectorAll(DROPPED).forEach((el) => el.remove());
3189
+ rows.querySelectorAll("*").forEach((el) => {
3190
+ for (const { name, value } of [...el.attributes]) {
3191
+ const lower = name.toLowerCase();
3192
+ const script = /^(java|vb)script:/i.test(value.replace(/[\u0000-\u0020]/g, ""));
3193
+ if (lower.startsWith("on") || script || LINK_ATTRIBUTES.has(lower) && !isSafeLink2(value)) {
3194
+ el.removeAttribute(name);
3195
+ }
3196
+ }
3197
+ });
3198
+ }
3199
+ var ROW_STYLE = "pointer-events: all; display: table; width: 100%;";
3200
+ function tablePreview(doc, attributes, rows) {
3201
+ const attr = (name, fallback) => String(attributes[name] ?? fallback);
3202
+ const template = doc.createElement("template");
3203
+ template.innerHTML = `<table>${rows}</table>`;
3204
+ const table = doc.createElement("table");
3205
+ table.setAttribute("cellpadding", String(parseInt(attr("cellpadding", "0"), 10) || 0));
3206
+ table.setAttribute("cellspacing", String(parseInt(attr("cellspacing", "0"), 10) || 0));
3207
+ Object.assign(table.style, {
3208
+ width: attr("width", "100%"),
3209
+ border: attr("border", "none"),
3210
+ color: attr("color", "#000000"),
3211
+ fontFamily: attr("font-family", "Ubuntu, Helvetica, Arial, sans-serif"),
3212
+ fontSize: attr("font-size", "13px"),
3213
+ lineHeight: attr("line-height", "22px"),
3214
+ tableLayout: attr("table-layout", "auto")
3215
+ });
3216
+ for (const row of template.content.querySelectorAll("tr")) {
3217
+ const tr = table.insertRow();
3218
+ for (const cell2 of row.querySelectorAll(":scope > th, :scope > td")) {
3219
+ const copy = doc.createElement(cell2.tagName.toLowerCase());
3220
+ copy.textContent = cell2.textContent;
3221
+ const align = cell2.getAttribute("align");
3222
+ if (align && /^(left|right|center)$/i.test(align)) copy.setAttribute("align", align);
3223
+ tr.append(copy);
3224
+ }
3225
+ }
3226
+ const cell = doc.createElement("td");
3227
+ cell.style.padding = attr("padding", "10px 25px");
3228
+ cell.append(table);
3229
+ return cell;
3230
+ }
3231
+ function addTableType(editor) {
3232
+ const base = editor.Components.getType("default").view.prototype;
3233
+ editor.Components.addType("mj-table", {
3234
+ isComponent: (el) => {
3235
+ if (el.tagName?.toLowerCase() !== "mj-table") return false;
3236
+ const rows = el.querySelector(":scope > template");
3237
+ if (rows) cleanRows(rows.content);
3238
+ return { type: "mj-table", content: rows?.innerHTML ?? "", components: [] };
3239
+ },
3240
+ model: { defaults: { tagName: "mj-table", droppable: false, editable: false } },
3241
+ view: {
3242
+ tagName: "tr",
3243
+ // GrapesJS clears the element's attributes, style among them, when the model's change.
3244
+ updateAttributes(...args) {
3245
+ base.updateAttributes.apply(this, args);
3246
+ this.el.style.cssText = ROW_STYLE;
3247
+ },
3248
+ updateContent() {
3249
+ const { el, model } = this;
3250
+ el.style.cssText = ROW_STYLE;
3251
+ el.replaceChildren(
3252
+ tablePreview(el.ownerDocument, model.getAttributes(), String(model.get("content") ?? ""))
3253
+ );
3254
+ }
3255
+ }
3256
+ });
3257
+ }
3258
+
3259
+ // src/components/editor/plugin-types.ts
3260
+ var COLUMN_CHILDREN = /* @__PURE__ */ new Set([
3261
+ "mj-accordion",
3262
+ "mj-button",
3263
+ "mj-carousel",
3264
+ "mj-divider",
3265
+ "mj-image",
3266
+ "mj-navbar",
3267
+ "mj-raw",
3268
+ "mj-social",
3269
+ "mj-spacer",
3270
+ "mj-table",
3271
+ "mj-text"
3272
+ ]);
3273
+ function fixPluginTypes(editor) {
3274
+ addTableType(editor);
3275
+ for (const type of editor.Components.getTypes()) {
3276
+ if (!type.id.startsWith("mj-") || type.id === "mj-table") continue;
3277
+ const base = type.model.prototype.getAttrToHTML;
3278
+ editor.Components.addType(type.id, {
3279
+ model: {
3280
+ defaults: {
3281
+ "style-default": {},
3282
+ ...type.id === "mj-column" && {
3283
+ droppable: (source) => COLUMN_CHILDREN.has(String(source.get("tagName")))
3284
+ },
3285
+ // GrapesJS's own is an SVG's markup, which the canvas wrote into `src` and saved.
3286
+ ...type.id === "mj-image" && { src: "" }
3287
+ },
3288
+ getAttrToHTML() {
3289
+ const attributes = base.call(this);
3290
+ delete attributes.__p;
3291
+ for (const [name, value] of Object.entries(attributes)) {
3292
+ if (typeof value === "string") attributes[name] = value.replace(/"/g, "&quot;");
3293
+ }
3294
+ return attributes;
3295
+ }
3296
+ }
3297
+ });
3298
+ }
3299
+ }
3300
+
3301
+ // src/components/editor/EmailEditor.tsx
3302
+ import { jsx as jsx8, jsxs as jsxs7 } from "react/jsx-runtime";
3303
+ var TYPE_LABEL = {
3304
+ "mj-text": "Text",
3305
+ "mj-button": "Button",
3306
+ "mj-image": "Image",
3307
+ "mj-column": "Column"
3308
+ };
3309
+ function tagOf6(c) {
3310
+ return String(c.get("tagName") || c.get("type") || "");
3311
+ }
3312
+ 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>';
3313
+ 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>';
3314
+ 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>';
3315
+ function isSection(c) {
3316
+ return tagOf6(c) === "mj-section";
3317
+ }
3318
+ function isCommentable(c) {
3319
+ const t = tagOf6(c);
3320
+ return t === "mj-section" || t in TYPE_LABEL;
3321
+ }
710
3322
  function closestSection(c) {
711
3323
  let cur = c;
712
3324
  while (cur) {
@@ -715,27 +3327,12 @@ function closestSection(c) {
715
3327
  }
716
3328
  return null;
717
3329
  }
718
- var CRUMB_NAME = {
719
- "mj-section": "Section",
720
- "mj-column": "Column",
721
- "mj-group": "Group",
722
- "mj-text": "Text",
723
- "mj-button": "Button",
724
- "mj-image": "Image",
725
- "mj-divider": "Divider",
726
- "mj-spacer": "Spacer",
727
- "mj-hero": "Hero",
728
- "mj-navbar": "Navbar",
729
- "mj-social": "Social",
730
- "mj-social-element": "Social",
731
- "mj-wrapper": "Wrapper"
732
- };
733
- function crumbName(c) {
734
- const t = tagOf(c);
735
- return CRUMB_NAME[t] ?? t.replace(/^mj-/, "") ?? "Element";
3330
+ function crumbName(c, labels) {
3331
+ const t = tagOf6(c);
3332
+ return labels.elementNames[t] ?? t.replace(/^mj-/, "");
736
3333
  }
737
3334
  function objectLabel(c) {
738
- const base = TYPE_LABEL[tagOf(c)] ?? tagOf(c);
3335
+ const base = TYPE_LABEL[tagOf6(c)] ?? tagOf6(c);
739
3336
  const text = String(c.getEl?.()?.textContent ?? "").replace(/\s+/g, " ").trim().slice(0, 30);
740
3337
  return text ? `${base}: \u201C${text}\u201D` : base;
741
3338
  }
@@ -767,75 +3364,6 @@ function wrapSelectionStyle(el, style) {
767
3364
  } catch {
768
3365
  }
769
3366
  }
770
- function setupStyleFields(editor, labels) {
771
- const sm = editor.StyleManager;
772
- const base = sm.getType("base").view;
773
- const documentColors = () => {
774
- const source = `${editor.getHtml()} ${editor.getCss() ?? ""}`;
775
- const counts = /* @__PURE__ */ new Map();
776
- const colorValue = /(?:color|background|border)[\w-]*\s*[:=]\s*["']?[^"';>{}]*?(#[0-9a-f]{3,8}\b|rgba?\([^)]*\))/gi;
777
- for (const match of source.matchAll(colorValue)) {
778
- const hex = toHex(match[1], { opaque: true });
779
- if (hex) counts.set(hex, (counts.get(hex) ?? 0) + 1);
780
- }
781
- return [...counts.entries()].sort((a, b) => b[1] - a[1]).slice(0, 14).map(([hex]) => hex);
782
- };
783
- const ownValue = (property) => property.hasValue?.({ noParent: true }) ? String(property.getValue?.() ?? "") : "";
784
- const reactView = (field) => base.extend({
785
- create({ property }) {
786
- const el = document.createElement("div");
787
- el.addEventListener("change", (event) => event.stopPropagation());
788
- const root = createRoot(el);
789
- this.__root = root;
790
- this.__render = () => root.render(field(property, ownValue(property)));
791
- this.__render();
792
- return el;
793
- },
794
- update() {
795
- this.__render?.();
796
- },
797
- destroy() {
798
- const root = this.__root;
799
- this.__root = void 0;
800
- this.__render = void 0;
801
- setTimeout(() => root?.unmount());
802
- }
803
- });
804
- sm.addType("color", {
805
- view: reactView((property, value) => /* @__PURE__ */ jsx5(
806
- ColorField,
807
- {
808
- value,
809
- placeholder: String(property.getDefaultValue?.() ?? ""),
810
- documentColors,
811
- labels,
812
- onChange: (next, partial) => property.upValue(next, { partial })
813
- }
814
- ))
815
- });
816
- sm.addType("file", {
817
- view: reactView((property, value) => /* @__PURE__ */ jsx5(
818
- ImageField,
819
- {
820
- value,
821
- labels,
822
- onClear: () => property.upValue(""),
823
- onChoose: () => {
824
- const am = editor.AssetManager;
825
- am.open({
826
- types: ["image"],
827
- current: imageUrl(value) ?? void 0,
828
- select(asset, complete) {
829
- const url = typeof asset === "string" ? asset : asset.getSrc();
830
- property.upValue(url, { partial: !complete });
831
- if (complete) am.close();
832
- }
833
- });
834
- }
835
- }
836
- ))
837
- });
838
- }
839
3367
  function setupRichText(editor, labels) {
840
3368
  const rte = editor.RichTextEditor;
841
3369
  if (!rte || rte.__customActions) return;
@@ -872,26 +3400,45 @@ function setupRichText(editor, labels) {
872
3400
  result: (r, action) => r.exec("hiliteColor", val(action, "input"))
873
3401
  });
874
3402
  }
875
- function EmailEditor({ docId, onReady, commentsRefresh, onOpenCountChange }) {
3403
+ function EmailEditor({
3404
+ docId,
3405
+ onReady,
3406
+ commentsRefresh,
3407
+ onOpenCountChange,
3408
+ blocks = []
3409
+ }) {
876
3410
  const documents = useDocumentStore();
877
3411
  const labels = useLabels();
878
- const loadingRef = useRef3(false);
879
- const saveStatusRef = useRef3("idle");
880
- const stateListeners = useRef3(/* @__PURE__ */ new Set());
881
- const [loading, setLoading] = useState3(true);
882
- const [sidebarView, setSidebarView] = useState3("blocks");
883
- const [composeTarget, setComposeTarget] = useState3(null);
884
- const selectionSource = useRef3("other");
885
- const editorRef = useRef3(null);
3412
+ const loadingRef = useRef5(false);
3413
+ const saveStatusRef = useRef5("idle");
3414
+ const stateListeners = useRef5(/* @__PURE__ */ new Set());
3415
+ const [loading, setLoading] = useState5(true);
3416
+ const [sidebarView, setSidebarView] = useState5("blocks");
3417
+ const blockSearchRef = useRef5(null);
3418
+ const onBlockSearchKey = (e) => {
3419
+ if (readOnlyRef.current || !opensBlockSearch(e)) return;
3420
+ e.preventDefault();
3421
+ setSidebarView("blocks");
3422
+ requestAnimationFrame(() => blockSearchRef.current?.focus());
3423
+ };
3424
+ const [composeTarget, setComposeTarget] = useState5(null);
3425
+ const selectionSource = useRef5("other");
3426
+ const editorRef = useRef5(null);
886
3427
  const { running: turnRunning, locked: readOnly, setLocked } = useAgentTurn();
887
- const readOnlyRef = useRef3(false);
888
- const applyReadOnly = useRef3(async () => {
3428
+ const readOnlyRef = useRef5(false);
3429
+ const applyReadOnly = useRef5(async () => {
889
3430
  });
890
- useEffect3(() => void applyReadOnly.current(turnRunning), [turnRunning]);
891
- useEffect3(() => () => setLocked(false), [setLocked]);
892
- const [crumbs, setCrumbs] = useState3([]);
893
- const [selectedImageSrc, setSelectedImageSrc] = useState3(null);
894
- const [picker, setPicker] = useState3(
3431
+ const [turn] = useState5(createTurnFollower);
3432
+ useEffect5(() => turn.turn(turnRunning), [turn, turnRunning]);
3433
+ useEffect5(() => {
3434
+ turn.mount();
3435
+ return () => {
3436
+ turn.unmount();
3437
+ setLocked(false);
3438
+ };
3439
+ }, [turn, setLocked]);
3440
+ const [crumbs, setCrumbs] = useState5([]);
3441
+ const [picker, setPicker] = useState5(
895
3442
  null
896
3443
  );
897
3444
  const onEditor = async (editor) => {
@@ -900,7 +3447,7 @@ function EmailEditor({ docId, onReady, commentsRefresh, onOpenCountChange }) {
900
3447
  const chain = [];
901
3448
  let cur = c;
902
3449
  while (cur) {
903
- const t = tagOf(cur);
3450
+ const t = tagOf6(cur);
904
3451
  if (!t || t === "mj-body" || t === "wrapper" || t === "Wrapper") break;
905
3452
  chain.unshift(cur);
906
3453
  cur = cur.parent() ?? void 0;
@@ -949,7 +3496,7 @@ function EmailEditor({ docId, onReady, commentsRefresh, onOpenCountChange }) {
949
3496
  if (isSection(c)) addIdClass(c, "sec");
950
3497
  if (!isCommentable(c)) return;
951
3498
  const toolbar = [...c.get("toolbar") ?? []];
952
- if (tagOf(c) === "mj-image" && !toolbar.some((t) => t.command === "open-image-picker")) {
3499
+ if (tagOf6(c) === "mj-image" && !toolbar.some((t) => t.command === "open-image-picker")) {
953
3500
  toolbar.push({
954
3501
  command: "open-image-picker",
955
3502
  label: IMAGE_SVG,
@@ -957,11 +3504,11 @@ function EmailEditor({ docId, onReady, commentsRefresh, onOpenCountChange }) {
957
3504
  });
958
3505
  c.set("toolbar", toolbar);
959
3506
  }
960
- if (!toolbar.some((t) => t.command === "open-comments")) {
3507
+ if (closestSection(c) && !toolbar.some((t) => t.command === "open-comments")) {
961
3508
  toolbar.push({
962
3509
  command: "open-comments",
963
3510
  label: SPARKLES_SVG,
964
- attributes: { title: "Comment on element / section", "data-role": "sparkles" }
3511
+ attributes: { title: labels.commentOnElement, "data-role": "sparkles" }
965
3512
  });
966
3513
  c.set("toolbar", toolbar);
967
3514
  }
@@ -969,7 +3516,7 @@ function EmailEditor({ docId, onReady, commentsRefresh, onOpenCountChange }) {
969
3516
  editor.Commands.add("open-image-picker", {
970
3517
  run(ed) {
971
3518
  const target = ed.getSelected();
972
- if (!target || tagOf(target) !== "mj-image") return;
3519
+ if (!target || tagOf6(target) !== "mj-image") return;
973
3520
  const am = ed.AssetManager;
974
3521
  am.open({
975
3522
  target,
@@ -988,21 +3535,29 @@ function EmailEditor({ docId, onReady, commentsRefresh, onOpenCountChange }) {
988
3535
  }
989
3536
  });
990
3537
  setupRichText(editor, labels);
991
- setupStyleFields(editor, labels);
992
- const blockCategory = (label) => {
993
- const l = label.toLowerCase();
994
- if (/(column|section)/.test(l)) return "Layout";
995
- if (/(text|image|button|divider|spacer|social)/.test(l)) return "Basic";
996
- return "Advanced";
3538
+ fixPluginTypes(editor);
3539
+ registerBlocks(editor, blocks, labels.blockCategorySections, SECTION_SVG);
3540
+ setupLayerNames(editor, labels);
3541
+ const STRUCTURE = /* @__PURE__ */ new Set(["mj-wrapper", "mj-section", "mj-column"]);
3542
+ const showBodyInLayers = () => {
3543
+ const body = findByTag("mj-body");
3544
+ if (!body) return;
3545
+ editor.Layers.setRoot(body);
3546
+ body.onAll((c) => {
3547
+ if (STRUCTURE.has(tagOf6(c))) editor.Layers.setOpen(c, true);
3548
+ });
997
3549
  };
998
- const bm = editor.BlockManager;
999
- bm.getAll().forEach(
1000
- (b) => {
1001
- b.set("category", blockCategory(b.get("label") || ""));
3550
+ const keepStructureOpen = () => {
3551
+ const layers = editor.Layers;
3552
+ const opened = layers.model?.get("opened") ?? {};
3553
+ for (const [cid, c] of Object.entries(opened)) {
3554
+ if (STRUCTURE.has(tagOf6(c))) delete opened[cid];
1002
3555
  }
1003
- );
1004
- bm.render();
1005
- editor.on("component:add", decorate);
3556
+ };
3557
+ editor.on("component:add", (c) => {
3558
+ if (!loadingRef.current) freshenCopiedIds(editor.getWrapper(), c);
3559
+ decorate(c);
3560
+ });
1006
3561
  editor.on("load", () => {
1007
3562
  const body = editor.Canvas.getBody();
1008
3563
  body?.addEventListener(
@@ -1012,45 +3567,28 @@ function EmailEditor({ docId, onReady, commentsRefresh, onOpenCountChange }) {
1012
3567
  },
1013
3568
  true
1014
3569
  );
3570
+ editor.Canvas.getDocument()?.addEventListener("keydown", onBlockSearchKey);
3571
+ showBodyInLayers();
1015
3572
  });
1016
3573
  editor.on("component:selected", (c) => {
3574
+ keepStructureOpen();
1017
3575
  if (selectionSource.current === "canvas") setSidebarView("settings");
1018
3576
  selectionSource.current = "other";
1019
3577
  buildCrumbs(c);
1020
- setSelectedImageSrc(c && tagOf(c) === "mj-image" ? String(c.get("src") ?? "") : null);
1021
- if (c?.get("type") === "mj-image" && !c.getTrait("src")) {
1022
- c.addTrait(
1023
- {
1024
- type: "text",
1025
- name: "src",
1026
- label: "Image (URL)",
1027
- placeholder: "https://\u2026",
1028
- changeProp: true
1029
- },
1030
- { at: 0 }
1031
- );
1032
- }
1033
3578
  });
1034
3579
  editor.on("component:deselected", () => {
1035
- const rest = editor.getSelected();
1036
- buildCrumbs(rest);
1037
- setSelectedImageSrc(
1038
- rest && tagOf(rest) === "mj-image" ? String(rest.get("src") ?? "") : null
1039
- );
1040
- });
1041
- editor.on("component:update:src", (c) => {
1042
- if (c === editor.getSelected()) setSelectedImageSrc(String(c.get("src") ?? ""));
3580
+ buildCrumbs(editor.getSelected());
1043
3581
  });
1044
- let stored = "";
3582
+ const baseline = createSaveBaseline();
1045
3583
  const save = async () => {
1046
3584
  const view = editor.getEditing()?.getView();
1047
3585
  await view?.syncContent?.();
1048
3586
  const mjml = editor.getHtml();
1049
- if (mjml === stored) return;
3587
+ if (!baseline.changed(mjml)) return;
1050
3588
  setSave("saving");
1051
3589
  try {
1052
3590
  await documents.save(docId, { mjml, projectData: editor.getProjectData() });
1053
- stored = mjml;
3591
+ baseline.saved(mjml);
1054
3592
  setSave("saved");
1055
3593
  } catch (e) {
1056
3594
  setSave("error");
@@ -1081,6 +3619,7 @@ function EmailEditor({ docId, onReady, commentsRefresh, onOpenCountChange }) {
1081
3619
  });
1082
3620
  }
1083
3621
  const lock = (on) => {
3622
+ if (!turn.mounted) return;
1084
3623
  readOnlyRef.current = on;
1085
3624
  setLocked(on);
1086
3625
  notifyState();
@@ -1106,24 +3645,36 @@ function EmailEditor({ docId, onReady, commentsRefresh, onOpenCountChange }) {
1106
3645
  return;
1107
3646
  }
1108
3647
  lock(false);
3648
+ queue.schedule();
1109
3649
  };
1110
3650
  const reload = () => queue.run(async () => {
1111
3651
  const fresh = await documents.get(docId);
1112
- if (fresh.mjml !== stored) loadMjml(fresh.mjml);
3652
+ if (fresh.mjml !== baseline.stored) loadMjml(fresh.mjml);
1113
3653
  });
1114
3654
  const loadMjml = (mjml) => {
1115
3655
  loadingRef.current = true;
3656
+ let repaired = false;
3657
+ const adds = !mjml || listSections(mjml).some((s) => !s.sectionId);
1116
3658
  try {
1117
3659
  queue.cancel();
1118
- stored = mjml;
1119
- editor.setComponents(mjml || STARTER_MJML);
1120
- editor.getWrapper()?.find("mj-section").forEach(decorate);
1121
- for (const t of Object.keys(TYPE_LABEL)) {
1122
- editor.getWrapper()?.find(t).forEach(decorate);
1123
- }
3660
+ baseline.load(mjml, null);
3661
+ const source = sourceForEditor(mjml || STARTER_MJML);
3662
+ repaired = source.repaired;
3663
+ editor.setComponents(source.mjml);
3664
+ editor.getWrapper()?.onAll((c) => {
3665
+ const illegal = illegalAttributes(tagOf6(c), c.getAttributes());
3666
+ if (illegal.length === 0) return;
3667
+ c.removeAttributes(illegal);
3668
+ repaired = true;
3669
+ });
3670
+ if (!repaired && !adds) baseline.load(mjml, editor.getHtml());
3671
+ showBodyInLayers();
3672
+ editor.UndoManager.clear();
1124
3673
  } finally {
1125
3674
  loadingRef.current = false;
1126
3675
  }
3676
+ notifyState();
3677
+ if ((repaired || adds) && !readOnlyRef.current) queue.schedule();
1127
3678
  };
1128
3679
  const ensureHighlightStyles = () => {
1129
3680
  const cdoc = editor.Canvas.getDocument();
@@ -1145,7 +3696,10 @@ function EmailEditor({ docId, onReady, commentsRefresh, onOpenCountChange }) {
1145
3696
  cdoc.head.appendChild(style);
1146
3697
  };
1147
3698
  const sectionEl = (sectionId) => {
1148
- const comp = editor.getWrapper()?.find("mj-section").find((c) => sectionIdOf(c) === sectionId);
3699
+ let comp;
3700
+ editor.getWrapper()?.onAll((c) => {
3701
+ if (!comp && isSection(c) && sectionIdOf(c) === sectionId) comp = c;
3702
+ });
1149
3703
  return comp?.getEl?.() ?? void 0;
1150
3704
  };
1151
3705
  const compiledHtml = () => {
@@ -1156,9 +3710,9 @@ function EmailEditor({ docId, onReady, commentsRefresh, onOpenCountChange }) {
1156
3710
  return editor.getHtml();
1157
3711
  }
1158
3712
  };
3713
+ turn.ready((on) => void applyReadOnly.current(on));
1159
3714
  try {
1160
- const doc = await documents.get(docId);
1161
- loadMjml(doc.mjml);
3715
+ await queue.run(async () => loadMjml((await documents.get(docId)).mjml));
1162
3716
  } catch (e) {
1163
3717
  toast2.error(labels.documentLoadFailed);
1164
3718
  console.error(e);
@@ -1210,10 +3764,10 @@ function EmailEditor({ docId, onReady, commentsRefresh, onOpenCountChange }) {
1210
3764
  }
1211
3765
  });
1212
3766
  };
1213
- return /* @__PURE__ */ jsx5(
3767
+ return /* @__PURE__ */ jsx8(
1214
3768
  GjsEditor,
1215
3769
  {
1216
- grapesjs,
3770
+ grapesjs: grapesjs2,
1217
3771
  className: "h-full",
1218
3772
  options: {
1219
3773
  height: "100%",
@@ -1222,12 +3776,13 @@ function EmailEditor({ docId, onReady, commentsRefresh, onOpenCountChange }) {
1222
3776
  // app.grapesjs.com and leave a session-storage entry, which a host would then have
1223
3777
  // to disclose and, for the entry, ask consent for.
1224
3778
  telemetry: false,
3779
+ canvasCss: CANVAS_CSS,
1225
3780
  fromElement: false,
1226
3781
  panels: { defaults: [] },
1227
3782
  blockManager: { appendTo: "#gjs-blocks" },
1228
- styleManager: { appendTo: "#gjs-styles" },
1229
- traitManager: { appendTo: "#gjs-traits" },
1230
- layerManager: { appendTo: "#gjs-layers" },
3783
+ // The eye hides a layer by writing `display="none"` into the MJML, which is illegal.
3784
+ // The root row is mj-body, which the sidebar reaches as "Email settings" instead.
3785
+ layerManager: { appendTo: "#gjs-layers", hidable: false, showWrapper: false },
1231
3786
  assetManager: {
1232
3787
  // Never inline files: mail clients block `data:` images (see ImageLibrary).
1233
3788
  embedAsBase64: false,
@@ -1241,115 +3796,118 @@ function EmailEditor({ docId, onReady, commentsRefresh, onOpenCountChange }) {
1241
3796
  close: () => setPicker(null)
1242
3797
  }
1243
3798
  },
3799
+ canvas: {
3800
+ customBadgeLabel: badgeLabel(labels),
3801
+ // A file or text dragged in from outside lands as `<img>` or `<div>`, which no MJML
3802
+ // validator accepts; drags from the palette are unaffected.
3803
+ allowExternalDrop: false
3804
+ },
1244
3805
  deviceManager: {
1245
3806
  devices: [
1246
3807
  { id: "desktop", name: "Desktop", width: "" },
1247
3808
  { id: "mobile", name: "Mobile", width: "375px", widthMedia: "480px" }
1248
3809
  ]
1249
3810
  },
1250
- plugins: [grapesjsMJML]
3811
+ plugins: [
3812
+ (editor) => grapesjsMJML2(editor, {
3813
+ blocks: BUILTIN_BLOCKS,
3814
+ block: builtinBlock(labels)
3815
+ })
3816
+ ]
1251
3817
  },
1252
3818
  onEditor,
1253
- children: /* @__PURE__ */ jsxs4("div", { className: "relative flex h-full min-h-0 flex-1", inert: readOnly, children: [
1254
- readOnly && /* @__PURE__ */ jsx5("div", { className: "absolute inset-0 z-30 flex cursor-not-allowed items-start justify-center bg-white/20 pt-3", children: /* @__PURE__ */ jsxs4("span", { className: "flex items-center gap-2 rounded-full bg-panel px-3 py-1 text-xs text-panel-fg shadow", children: [
1255
- /* @__PURE__ */ jsx5(Spinner, {}),
1256
- " ",
1257
- labels.agentWorking
1258
- ] }) }),
1259
- /* @__PURE__ */ jsx5(
1260
- LeftSidebar,
1261
- {
1262
- view: sidebarView,
1263
- onViewChange: setSidebarView,
1264
- markLayersSource: () => selectionSource.current = "layers",
1265
- selectedImageSrc,
1266
- onChooseImage: () => editorRef.current?.runCommand("open-image-picker")
1267
- }
1268
- ),
1269
- /* @__PURE__ */ jsxs4("div", { className: "flex min-w-0 flex-1 flex-col bg-surface-muted", children: [
1270
- /* @__PURE__ */ jsxs4("div", { className: "relative min-h-0 flex-1", children: [
1271
- /* @__PURE__ */ jsx5(Canvas, { className: "h-full" }),
1272
- /* @__PURE__ */ jsx5(
1273
- CanvasComments,
1274
- {
1275
- docId,
1276
- composeTarget,
1277
- onComposeConsumed: () => setComposeTarget(null),
1278
- refreshSignal: commentsRefresh,
1279
- onOpenCountChange
1280
- }
1281
- ),
1282
- /* @__PURE__ */ jsx5(
1283
- ImagePicker,
3819
+ children: /* @__PURE__ */ jsxs7(
3820
+ "div",
3821
+ {
3822
+ className: "relative flex h-full min-h-0 flex-1",
3823
+ inert: readOnly,
3824
+ onKeyDown: (e) => onBlockSearchKey(e.nativeEvent),
3825
+ children: [
3826
+ readOnly && /* @__PURE__ */ jsx8("div", { className: "absolute inset-0 z-30 flex cursor-not-allowed items-start justify-center bg-white/20 pt-3", children: /* @__PURE__ */ jsxs7("span", { className: "flex items-center gap-2 rounded-full bg-panel px-3 py-1 text-xs text-panel-fg shadow", children: [
3827
+ /* @__PURE__ */ jsx8(Spinner, {}),
3828
+ " ",
3829
+ labels.agentWorking
3830
+ ] }) }),
3831
+ /* @__PURE__ */ jsx8(
3832
+ LeftSidebar,
1284
3833
  {
1285
- open: picker !== null,
1286
- currentUrl: picker?.current,
1287
- onSelect: (url) => {
1288
- if (!readOnlyRef.current) picker?.select(url);
1289
- setPicker(null);
1290
- },
1291
- onClose: () => {
1292
- setPicker(null);
1293
- editorRef.current?.AssetManager.close();
1294
- }
3834
+ view: sidebarView,
3835
+ onViewChange: setSidebarView,
3836
+ markLayersSource: () => selectionSource.current = "layers",
3837
+ blockSearchRef
1295
3838
  }
1296
3839
  ),
1297
- loading && /* @__PURE__ */ jsxs4("div", { className: "absolute inset-0 z-10 flex items-center justify-center gap-2 bg-white/70 text-sm text-zinc-500", children: [
1298
- /* @__PURE__ */ jsx5(Spinner, {}),
1299
- " Loading editor\u2026"
3840
+ /* @__PURE__ */ jsxs7("div", { className: "flex min-w-0 flex-1 flex-col bg-surface-muted", children: [
3841
+ /* @__PURE__ */ jsxs7("div", { className: "relative min-h-0 flex-1", children: [
3842
+ /* @__PURE__ */ jsx8(Canvas, { className: "h-full" }),
3843
+ /* @__PURE__ */ jsx8(
3844
+ CanvasComments,
3845
+ {
3846
+ docId,
3847
+ composeTarget,
3848
+ onComposeConsumed: () => setComposeTarget(null),
3849
+ refreshSignal: commentsRefresh,
3850
+ onOpenCountChange
3851
+ }
3852
+ ),
3853
+ /* @__PURE__ */ jsx8(
3854
+ ImagePicker,
3855
+ {
3856
+ open: picker !== null,
3857
+ currentUrl: picker?.current,
3858
+ onSelect: (url) => {
3859
+ if (!readOnlyRef.current) picker?.select(url);
3860
+ setPicker(null);
3861
+ },
3862
+ onClose: () => {
3863
+ setPicker(null);
3864
+ editorRef.current?.AssetManager.close();
3865
+ }
3866
+ }
3867
+ ),
3868
+ loading && /* @__PURE__ */ jsxs7("div", { className: "absolute inset-0 z-10 flex items-center justify-center gap-2 bg-white/70 text-sm text-zinc-500", children: [
3869
+ /* @__PURE__ */ jsx8(Spinner, {}),
3870
+ " ",
3871
+ labels.loadingEditor
3872
+ ] })
3873
+ ] }),
3874
+ /* @__PURE__ */ jsx8(Breadcrumb, { crumbs, onSelect: (c) => editorRef.current?.select(c) })
1300
3875
  ] })
1301
- ] }),
1302
- /* @__PURE__ */ jsx5(Breadcrumb, { crumbs, onSelect: (c) => editorRef.current?.select(c) })
1303
- ] })
1304
- ] })
3876
+ ]
3877
+ }
3878
+ )
1305
3879
  }
1306
3880
  );
1307
3881
  }
1308
- function CollapseSection({
1309
- title,
1310
- defaultOpen = true,
1311
- children
1312
- }) {
1313
- const [open, setOpen] = useState3(defaultOpen);
1314
- return /* @__PURE__ */ jsxs4("div", { className: "border-b border-panel-border", children: [
1315
- /* @__PURE__ */ jsxs4(
1316
- "button",
1317
- {
1318
- type: "button",
1319
- onClick: () => setOpen((o) => !o),
1320
- 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",
1321
- children: [
1322
- /* @__PURE__ */ jsx5(ChevronRight, { className: cn("size-3.5 transition-transform", open && "rotate-90") }),
1323
- title
1324
- ]
1325
- }
1326
- ),
1327
- /* @__PURE__ */ jsx5("div", { className: cn(!open && "hidden"), children })
1328
- ] });
1329
- }
1330
3882
  function LeftSidebar({
1331
3883
  view,
1332
3884
  onViewChange,
1333
3885
  markLayersSource,
1334
- selectedImageSrc,
1335
- onChooseImage
3886
+ blockSearchRef
1336
3887
  }) {
1337
- const editor = useEditorMaybe();
3888
+ const editor = useEditorMaybe2();
1338
3889
  const labels = useLabels();
1339
- const [layersExpanded, setLayersExpanded] = useState3(false);
1340
- const [blockSearch, setBlockSearch] = useState3("");
1341
- const changeView = (v) => {
1342
- if (v === "settings" && editor && !editor.getSelected()) {
1343
- const wrapper = editor.getWrapper();
1344
- const body = wrapper?.find("mj-body")[0] ?? wrapper;
1345
- if (body) editor.select(body);
1346
- }
1347
- onViewChange(v);
3890
+ const [layersExpanded, setLayersExpanded] = useState5(false);
3891
+ const [blockSearch, setBlockSearch] = useState5("");
3892
+ const [noBlockMatches, setNoBlockMatches] = useState5(false);
3893
+ const selectBody = () => {
3894
+ let body;
3895
+ editor?.getWrapper()?.onAll((c) => {
3896
+ if (!body && tagOf6(c) === "mj-body") body = c;
3897
+ });
3898
+ if (body) editor?.select(body);
1348
3899
  };
1349
3900
  const toggleLayers = (expand) => {
1350
3901
  setLayersExpanded(expand);
1351
3902
  editor?.getWrapper()?.onAll((c) => c.set("open", expand));
1352
3903
  };
3904
+ const onTileKey = (e) => {
3905
+ const tile = e.target.closest("[data-block]");
3906
+ const block = tile && editor?.Blocks.get(tile.dataset.block ?? "");
3907
+ if (!editor || !block || !addsBlock(e)) return;
3908
+ e.preventDefault();
3909
+ if (insertBlock(editor, block)) toast2(labels.blockAdded(tile.textContent?.trim() ?? ""));
3910
+ };
1353
3911
  const filterBlocks = (q) => {
1354
3912
  setBlockSearch(q);
1355
3913
  const root = document.getElementById("gjs-blocks");
@@ -1365,17 +3923,22 @@ function LeftSidebar({
1365
3923
  );
1366
3924
  cat.style.display = anyVisible ? "" : "none";
1367
3925
  });
3926
+ setNoBlockMatches(
3927
+ [...root.querySelectorAll(".gjs-block")].every(
3928
+ (b) => b.style.display === "none"
3929
+ )
3930
+ );
1368
3931
  };
1369
- return /* @__PURE__ */ jsxs4("div", { className: "editor-dark flex w-64 shrink-0 flex-col border-r border-panel-border bg-panel text-panel-fg", children: [
1370
- /* @__PURE__ */ jsx5("div", { className: "flex border-b border-panel-border", children: [
1371
- ["blocks", "Blocks"],
1372
- ["settings", "Settings"],
1373
- ["layers", "Layers"]
1374
- ].map(([value, label]) => /* @__PURE__ */ jsx5(
3932
+ return /* @__PURE__ */ jsxs7("div", { className: "editor-dark flex w-64 shrink-0 flex-col border-r border-panel-border bg-panel text-panel-fg", children: [
3933
+ /* @__PURE__ */ jsx8("div", { className: "flex border-b border-panel-border", children: [
3934
+ ["blocks", labels.tabBlocks],
3935
+ ["settings", labels.tabSettings],
3936
+ ["layers", labels.tabLayers]
3937
+ ].map(([value, label]) => /* @__PURE__ */ jsx8(
1375
3938
  "button",
1376
3939
  {
1377
3940
  type: "button",
1378
- onClick: () => changeView(value),
3941
+ onClick: () => onViewChange(value),
1379
3942
  className: cn(
1380
3943
  "relative flex-1 px-3 py-2.5 text-sm transition-colors",
1381
3944
  "after:absolute after:inset-x-0 after:-bottom-px after:h-0.5 after:transition-colors",
@@ -1385,70 +3948,57 @@ function LeftSidebar({
1385
3948
  },
1386
3949
  value
1387
3950
  )) }),
1388
- /* @__PURE__ */ jsxs4("div", { className: cn("flex min-h-0 flex-1 flex-col", view !== "blocks" && "hidden"), children: [
1389
- /* @__PURE__ */ jsxs4("div", { className: "relative border-b border-panel-border p-2", children: [
1390
- /* @__PURE__ */ jsx5(Search, { className: "pointer-events-none absolute top-1/2 left-4 size-3.5 -translate-y-1/2 text-panel-muted-fg" }),
1391
- /* @__PURE__ */ jsx5(
3951
+ /* @__PURE__ */ jsxs7("div", { className: cn("flex min-h-0 flex-1 flex-col", view !== "blocks" && "hidden"), children: [
3952
+ /* @__PURE__ */ jsxs7("div", { className: "relative border-b border-panel-border p-2", children: [
3953
+ /* @__PURE__ */ jsx8(Search, { className: "pointer-events-none absolute top-1/2 left-4 size-3.5 -translate-y-1/2 text-panel-muted-fg" }),
3954
+ /* @__PURE__ */ jsx8(
1392
3955
  Input,
1393
3956
  {
3957
+ ref: blockSearchRef,
1394
3958
  value: blockSearch,
1395
3959
  onChange: (e) => filterBlocks(e.target.value),
1396
3960
  placeholder: labels.searchBlocks,
1397
3961
  className: "h-8 border-panel-border bg-panel-elevated pr-10 pl-7 text-sm text-panel-fg placeholder:text-panel-muted-fg"
1398
3962
  }
1399
3963
  ),
1400
- /* @__PURE__ */ jsx5("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" })
1401
- ] }),
1402
- /* @__PURE__ */ jsx5("div", { id: "gjs-blocks", className: "min-h-0 flex-1 overflow-y-auto" })
1403
- ] }),
1404
- /* @__PURE__ */ jsxs4("div", { className: cn("min-h-0 flex-1 overflow-y-auto", view !== "settings" && "hidden"), children: [
1405
- selectedImageSrc !== null && /* @__PURE__ */ jsxs4("div", { className: "border-b border-panel-border p-3", children: [
1406
- /* @__PURE__ */ jsxs4(
1407
- "button",
1408
- {
1409
- type: "button",
1410
- onClick: onChooseImage,
1411
- title: labels.chooseImage,
1412
- className: "group relative block w-full overflow-hidden rounded-lg border border-panel-border bg-panel-elevated",
1413
- children: [
1414
- selectedImageSrc ? /* @__PURE__ */ jsx5(
1415
- "img",
1416
- {
1417
- src: selectedImageSrc,
1418
- alt: "",
1419
- className: "h-32 w-full bg-black/25 object-contain"
1420
- }
1421
- ) : /* @__PURE__ */ jsx5("span", { className: "flex h-32 items-center justify-center text-panel-muted-fg", children: /* @__PURE__ */ jsx5(ImageIcon2, { className: "size-8" }) }),
1422
- /* @__PURE__ */ jsx5("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 })
1423
- ]
1424
- }
1425
- ),
1426
- /* @__PURE__ */ jsxs4(
1427
- Button,
1428
- {
1429
- type: "button",
1430
- size: "sm",
1431
- className: "mt-2 w-full bg-brand text-brand-fg hover:bg-brand/90",
1432
- onClick: onChooseImage,
1433
- children: [
1434
- /* @__PURE__ */ jsx5(ImageUp2, {}),
1435
- labels.chooseImage
1436
- ]
1437
- }
1438
- )
3964
+ /* @__PURE__ */ jsx8("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: "/" })
1439
3965
  ] }),
1440
- /* @__PURE__ */ jsx5(CollapseSection, { title: labels.attributes, children: /* @__PURE__ */ jsx5("div", { id: "gjs-traits" }) }),
1441
- /* @__PURE__ */ jsx5(CollapseSection, { title: labels.style, children: /* @__PURE__ */ jsx5("div", { id: "gjs-styles" }) })
3966
+ noBlockMatches && /* @__PURE__ */ jsx8("p", { className: "px-3 py-6 text-center text-xs text-panel-muted-fg", children: labels.noBlocksMatch }),
3967
+ /* @__PURE__ */ jsx8("div", { id: "gjs-blocks", className: "min-h-0 flex-1 overflow-y-auto", onKeyDown: onTileKey })
1442
3968
  ] }),
1443
- /* @__PURE__ */ jsxs4(
3969
+ /* @__PURE__ */ jsx8("div", { className: cn("min-h-0 flex-1 overflow-y-auto", view !== "settings" && "hidden"), children: /* @__PURE__ */ jsx8(
3970
+ Inspector,
3971
+ {
3972
+ onEmailSettings: selectBody,
3973
+ empty: /* @__PURE__ */ jsxs7("div", { className: "flex flex-col items-center gap-3 px-6 py-10 text-center", children: [
3974
+ /* @__PURE__ */ jsx8(MousePointerClick, { className: "size-6 text-panel-muted-fg" }),
3975
+ /* @__PURE__ */ jsx8("p", { className: "text-sm text-panel-muted-fg", children: labels.settingsEmpty }),
3976
+ /* @__PURE__ */ jsxs7(
3977
+ Button,
3978
+ {
3979
+ type: "button",
3980
+ size: "sm",
3981
+ variant: "outline",
3982
+ className: "border-panel-border bg-panel-elevated text-panel-fg hover:bg-panel-hover hover:text-panel-fg",
3983
+ onClick: selectBody,
3984
+ children: [
3985
+ /* @__PURE__ */ jsx8(Mail, {}),
3986
+ labels.emailSettings
3987
+ ]
3988
+ }
3989
+ )
3990
+ ] })
3991
+ }
3992
+ ) }),
3993
+ /* @__PURE__ */ jsxs7(
1444
3994
  "div",
1445
3995
  {
1446
3996
  className: cn("flex min-h-0 flex-1 flex-col", view !== "layers" && "hidden"),
1447
3997
  onMouseDownCapture: markLayersSource,
1448
3998
  children: [
1449
- /* @__PURE__ */ jsxs4("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: [
1450
- /* @__PURE__ */ jsx5("span", { children: labels.expandAll }),
1451
- /* @__PURE__ */ jsx5(
3999
+ /* @__PURE__ */ jsxs7("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: [
4000
+ /* @__PURE__ */ jsx8("span", { children: labels.expandAll }),
4001
+ /* @__PURE__ */ jsx8(
1452
4002
  Switch,
1453
4003
  {
1454
4004
  checked: layersExpanded,
@@ -1457,7 +4007,16 @@ function LeftSidebar({
1457
4007
  }
1458
4008
  )
1459
4009
  ] }),
1460
- /* @__PURE__ */ jsx5("div", { id: "gjs-layers", className: "min-h-0 flex-1 overflow-y-auto" })
4010
+ /* @__PURE__ */ jsx8(
4011
+ "div",
4012
+ {
4013
+ id: "gjs-layers",
4014
+ className: "min-h-0 flex-1 overflow-y-auto",
4015
+ onDoubleClickCapture: (e) => {
4016
+ if (e.target.closest("[data-name]")) e.stopPropagation();
4017
+ }
4018
+ }
4019
+ )
1461
4020
  ]
1462
4021
  }
1463
4022
  )
@@ -1468,9 +4027,9 @@ function Breadcrumb({
1468
4027
  onSelect
1469
4028
  }) {
1470
4029
  const labels = useLabels();
1471
- return /* @__PURE__ */ jsx5("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__ */ jsx5("span", { className: "text-muted-foreground/60", children: labels.selectAnElement }) : crumbs.map((c, i) => /* @__PURE__ */ jsxs4("span", { className: "flex items-center gap-0.5", children: [
1472
- i > 0 && /* @__PURE__ */ jsx5(ChevronRight, { className: "size-3 opacity-40" }),
1473
- /* @__PURE__ */ jsx5(
4030
+ return /* @__PURE__ */ jsx8("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__ */ jsx8("span", { className: "text-muted-foreground/60", children: labels.selectAnElement }) : crumbs.map((c, i) => /* @__PURE__ */ jsxs7("span", { className: "flex items-center gap-0.5", children: [
4031
+ i > 0 && /* @__PURE__ */ jsx8(ChevronRight3, { className: "size-3 opacity-40" }),
4032
+ /* @__PURE__ */ jsx8(
1474
4033
  "button",
1475
4034
  {
1476
4035
  type: "button",
@@ -1479,12 +4038,13 @@ function Breadcrumb({
1479
4038
  "rounded px-1.5 py-0.5 hover:bg-muted hover:text-foreground",
1480
4039
  i === crumbs.length - 1 && "font-medium text-brand"
1481
4040
  ),
1482
- children: crumbName(c)
4041
+ children: crumbName(c, labels)
1483
4042
  }
1484
4043
  )
1485
4044
  ] }, i)) });
1486
4045
  }
1487
4046
  export {
1488
- EmailEditor
4047
+ EmailEditor,
4048
+ blockProblems
1489
4049
  };
1490
4050
  //# sourceMappingURL=canvas.js.map