@mjml-agent-editor/editor 0.1.16 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/canvas.js CHANGED
@@ -16,19 +16,23 @@ import {
16
16
  useDocumentStore,
17
17
  useImageLibrary,
18
18
  useLabels
19
- } from "./chunk-W3HCLTFJ.js";
19
+ } from "./chunk-FTEJNUJL.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
32
  import grapesjs from "grapesjs";
29
- import GjsEditor, { Canvas, useEditorMaybe } from "@grapesjs/react";
33
+ import GjsEditor, { Canvas, useEditorMaybe as useEditorMaybe2 } from "@grapesjs/react";
30
34
  import grapesjsMJML from "grapesjs-mjml";
31
- import { useEffect as useEffect3, useRef as useRef3, useState as useState3 } from "react";
35
+ import { useEffect as useEffect5, useRef as useRef5, useState as useState5 } from "react";
32
36
  import { STARTER_MJML } from "@mjml-agent-editor/core";
33
37
 
34
38
  // src/lib/save-queue.ts
@@ -60,9 +64,17 @@ function createSaveQueue(save, delayMs) {
60
64
  };
61
65
  }
62
66
 
67
+ // src/lib/shortcuts.ts
68
+ function opensBlockSearch(e) {
69
+ if (e.key !== "/" || e.metaKey || e.ctrlKey || e.altKey || e.isComposing) return false;
70
+ const target = e.target;
71
+ if (target?.isContentEditable) return false;
72
+ return !["INPUT", "TEXTAREA", "SELECT", "IFRAME"].includes(target?.tagName ?? "");
73
+ }
74
+
63
75
  // src/components/editor/EmailEditor.tsx
64
76
  import { toast as toast2 } from "sonner";
65
- import { ChevronRight, ImageIcon as ImageIcon2, ImageUp as ImageUp2 } from "lucide-react";
77
+ import { ChevronRight as ChevronRight3, Mail, MousePointerClick } from "lucide-react";
66
78
 
67
79
  // src/components/ui/switch.tsx
68
80
  import "react";
@@ -353,11 +365,1042 @@ function matchesAccept(file, accept) {
353
365
  });
354
366
  }
355
367
 
368
+ // src/components/editor/inspector/Inspector.tsx
369
+ import { useEffect as useEffect4, useLayoutEffect, useReducer, useRef as useRef4, useState as useState4 } from "react";
370
+ import { useEditorMaybe } from "@grapesjs/react";
371
+ import { ArrowUp, ChevronRight as ChevronRight2, Copy, Monitor, Trash2 as Trash22, X as X2 } from "lucide-react";
372
+
356
373
  // src/components/editor/ColorField.tsx
357
- import { useCallback as useCallback2, useEffect as useEffect2, useRef as useRef2, useState as useState2 } from "react";
374
+ import { useCallback as useCallback2, useEffect as useEffect3, useRef as useRef3, useState as useState3 } from "react";
358
375
  import { HexAlphaColorPicker, HexColorPicker } from "react-colorful";
359
376
  import { Pipette } from "lucide-react";
377
+
378
+ // src/components/editor/inspector/controls.tsx
379
+ import {
380
+ createContext,
381
+ useContext,
382
+ useEffect as useEffect2,
383
+ useId,
384
+ useRef as useRef2,
385
+ useState as useState2
386
+ } from "react";
387
+ import {
388
+ AlignCenter,
389
+ AlignJustify,
390
+ AlignLeft,
391
+ AlignRight,
392
+ ChevronDown,
393
+ ChevronUp,
394
+ CircleHelp,
395
+ Grid2x2,
396
+ RotateCcw
397
+ } from "lucide-react";
398
+
399
+ // src/components/editor/inspector/values.ts
400
+ var SIDES = ["top", "right", "bottom", "left"];
401
+ function expandBox(value) {
402
+ const parts = value.trim().split(/\s+/).filter(Boolean);
403
+ if (parts.length === 0 || parts.length > 4) return null;
404
+ const [t, r = t, b = t, l = r] = parts;
405
+ return [t, r, b, l];
406
+ }
407
+ function readBox(attributes, attr) {
408
+ const short = expandBox(attributes[attr] ?? "") ?? ["", "", "", ""];
409
+ return SIDES.map((side, i) => attributes[`${attr}-${side}`] ?? short[i]);
410
+ }
411
+ function boxShorthand(sides) {
412
+ const [t, r, b, l] = sides.map((s) => s.trim() || "0");
413
+ if (t === b && r === l) return t === r ? t : `${t} ${r}`;
414
+ if (r === l) return `${t} ${r} ${b}`;
415
+ return `${t} ${r} ${b} ${l}`;
416
+ }
417
+ function parseLength(value, units) {
418
+ const match = /^(-?\d*\.?\d+)\s*(px|%|em)?$/.exec(value.trim());
419
+ if (!match) return null;
420
+ const unit = match[2] ?? "";
421
+ return units.includes(unit) ? { amount: match[1], unit } : null;
422
+ }
423
+ var NO_SHARE = "-noshare";
424
+ function parseNetwork(name) {
425
+ return name.endsWith(NO_SHARE) ? { network: name.slice(0, -NO_SHARE.length), share: false } : { network: name, share: name !== "custom" && name !== "" };
426
+ }
427
+ function networkName(network, share) {
428
+ return share || network === "custom" ? network : `${network}${NO_SHARE}`;
429
+ }
430
+ var LINK_SCHEMES = /* @__PURE__ */ new Set(["http", "https", "mailto", "tel", "sms"]);
431
+ function isSafeLink(value) {
432
+ const decoded = value.replace(/&#x([0-9a-f]+);?/gi, (_, hex) => String.fromCodePoint(parseInt(hex, 16))).replace(/&#(\d+);?/g, (_, dec) => String.fromCodePoint(Number(dec))).replace(/:/g, ":").replace(/&(Tab|NewLine);/g, "").replace(/[\u0000-\u0020]/g, "");
433
+ const scheme = /^([a-z][a-z0-9+.-]*):/i.exec(decoded)?.[1];
434
+ return !scheme || LINK_SCHEMES.has(scheme.toLowerCase());
435
+ }
436
+ function isSafeNavbarLink(baseUrl, href) {
437
+ return isSafeLink(baseUrl) && isSafeLink(href) && isSafeLink(baseUrl + href);
438
+ }
439
+
440
+ // src/components/editor/inspector/controls.tsx
360
441
  import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
442
+ 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";
443
+ var FieldLabel = createContext({});
444
+ var useFieldLabel = () => {
445
+ const { label, help } = useContext(FieldLabel);
446
+ return { "aria-labelledby": label, "aria-describedby": help };
447
+ };
448
+ function Row({
449
+ label,
450
+ help,
451
+ own,
452
+ wide,
453
+ inline,
454
+ labels,
455
+ onReset,
456
+ children
457
+ }) {
458
+ const id = useId();
459
+ const helpId = useId();
460
+ const [helpOpen, setHelpOpen] = useState2(false);
461
+ const control = /* @__PURE__ */ jsx3(FieldLabel.Provider, { value: { label: id, help: help ? helpId : void 0 }, children });
462
+ const caption = /* @__PURE__ */ jsxs2("span", { className: "flex min-w-0 items-center gap-1.5 text-[11px] text-panel-muted-fg", children: [
463
+ /* @__PURE__ */ jsx3(
464
+ "span",
465
+ {
466
+ "aria-hidden": true,
467
+ className: cn("size-1.5 shrink-0 rounded-full", own ? "bg-brand" : "bg-transparent")
468
+ }
469
+ ),
470
+ /* @__PURE__ */ jsx3("span", { id, className: "truncate", children: label }),
471
+ help && // Its own provider: a host need not wrap the editor in one, as only EditorHeader asked.
472
+ // Opened by a tap as well: Radix opens a tooltip on hover and focus, not on touch.
473
+ /* @__PURE__ */ jsx3(TooltipProvider, { children: /* @__PURE__ */ jsxs2(Tooltip, { open: helpOpen, onOpenChange: setHelpOpen, children: [
474
+ /* @__PURE__ */ jsx3(TooltipTrigger, { asChild: true, children: /* @__PURE__ */ jsx3(
475
+ "button",
476
+ {
477
+ type: "button",
478
+ "aria-label": help,
479
+ onClick: () => setHelpOpen((open) => !open),
480
+ className: "-m-0.5 shrink-0 rounded p-0.5 text-panel-muted-fg/70 hover:text-panel-fg focus-visible:text-panel-fg",
481
+ children: /* @__PURE__ */ jsx3(CircleHelp, { className: "size-3" })
482
+ }
483
+ ) }),
484
+ /* @__PURE__ */ jsx3(TooltipContent, { side: "top", className: "max-w-56 leading-snug", children: help })
485
+ ] }) }),
486
+ own && /* @__PURE__ */ jsx3(
487
+ "button",
488
+ {
489
+ type: "button",
490
+ onClick: onReset,
491
+ title: labels.resetToDefault,
492
+ "aria-label": `${labels.resetToDefault}: ${label}`,
493
+ 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",
494
+ children: /* @__PURE__ */ jsx3(RotateCcw, { className: "size-3" })
495
+ }
496
+ )
497
+ ] });
498
+ return /* @__PURE__ */ jsxs2("div", { className: "group/row px-3 py-1", children: [
499
+ wide ? /* @__PURE__ */ jsxs2("div", { className: "flex flex-col gap-1", children: [
500
+ caption,
501
+ control
502
+ ] }) : inline ? /* @__PURE__ */ jsxs2("div", { className: "flex items-center justify-between gap-2", children: [
503
+ caption,
504
+ control
505
+ ] }) : /* @__PURE__ */ jsxs2("div", { className: "grid grid-cols-[92px_minmax(0,1fr)] items-center gap-2", children: [
506
+ caption,
507
+ control
508
+ ] }),
509
+ help && /* @__PURE__ */ jsx3("span", { id: helpId, hidden: true, children: help })
510
+ ] });
511
+ }
512
+ function useDraft(value, commit, source = value) {
513
+ const [draft, setDraft] = useState2(value);
514
+ useEffect2(() => setDraft(value), [value, source]);
515
+ const done = () => {
516
+ if (draft.trim() !== value) commit(draft.trim());
517
+ };
518
+ const onKeyDown = (e) => {
519
+ if (e.key === "Enter") e.currentTarget.blur();
520
+ if (e.key === "Escape") {
521
+ setDraft(value);
522
+ requestAnimationFrame(() => e.target.blur());
523
+ }
524
+ };
525
+ return { draft, setDraft, done, onKeyDown };
526
+ }
527
+ function TextInput({
528
+ value,
529
+ placeholder,
530
+ accept,
531
+ onCommit
532
+ }) {
533
+ const [invalid, setInvalid] = useState2(false);
534
+ const field = useFieldLabel();
535
+ const { draft, setDraft, done, onKeyDown } = useDraft(value, (next) => {
536
+ const ok = !accept || accept(next);
537
+ setInvalid(!ok);
538
+ if (ok) onCommit(next);
539
+ });
540
+ return /* @__PURE__ */ jsx3(
541
+ "input",
542
+ {
543
+ className: cn(FIELD, invalid && "border-red-400"),
544
+ ...field,
545
+ "aria-invalid": invalid,
546
+ value: draft,
547
+ placeholder,
548
+ onChange: (e) => setDraft(e.target.value),
549
+ onBlur: done,
550
+ onKeyDown
551
+ }
552
+ );
553
+ }
554
+ var BARE_NUMBER = /^-?\d*\.?\d+$/;
555
+ function normaliseLength(text, units, negative = false, unit = units[0]) {
556
+ if (text === "") return "";
557
+ const parsed = BARE_NUMBER.test(text) ? { amount: text, unit } : parseLength(text, units);
558
+ if (!parsed || !negative && parsed.amount.startsWith("-")) return null;
559
+ return `${parsed.amount}${parsed.unit}`;
560
+ }
561
+ function stepAmount(amount, delta, negative = false) {
562
+ const moved = Math.round((Number(amount) + delta) * 100) / 100;
563
+ const next = String(negative ? moved : Math.max(0, moved));
564
+ return BARE_NUMBER.test(next) ? next : amount;
565
+ }
566
+ var REPEAT_AFTER_MS = 400;
567
+ var REPEAT_EVERY_MS = 60;
568
+ function LengthInput({
569
+ value,
570
+ placeholder,
571
+ units,
572
+ negative = false,
573
+ compact = false,
574
+ labelledBy,
575
+ labels,
576
+ onCommit
577
+ }) {
578
+ const fieldLabel = useFieldLabel();
579
+ const own = parseLength(value, units);
580
+ const fallback = parseLength(placeholder ?? "", units);
581
+ const [picked, setPicked] = useState2(null);
582
+ const unit = own?.unit ?? picked ?? fallback?.unit ?? units[0];
583
+ const [invalid, setInvalid] = useState2(false);
584
+ const commit = useRef2(onCommit);
585
+ commit.current = onCommit;
586
+ const endHold = useRef2(null);
587
+ useEffect2(() => () => endHold.current?.(), []);
588
+ const { draft, setDraft, done, onKeyDown } = useDraft(
589
+ own ? own.amount : value,
590
+ (next) => {
591
+ endHold.current?.();
592
+ const normal = normaliseLength(next, units, negative, unit);
593
+ setInvalid(normal === null);
594
+ if (normal !== null) commit.current(normal);
595
+ },
596
+ value
597
+ );
598
+ const from = () => {
599
+ const text = draft.trim();
600
+ if (BARE_NUMBER.test(text)) return { amount: text, unit };
601
+ if (text === "") return { amount: fallback?.amount ?? "0", unit };
602
+ return parseLength(text, units);
603
+ };
604
+ const stepOnce = (delta) => {
605
+ endHold.current?.();
606
+ const start = from();
607
+ if (!start) return setInvalid(true);
608
+ const amount = stepAmount(start.amount, delta, negative);
609
+ setInvalid(false);
610
+ setDraft(amount);
611
+ commit.current(`${amount}${start.unit}`);
612
+ };
613
+ const onKey = (e) => {
614
+ if (e.key !== "ArrowUp" && e.key !== "ArrowDown") return onKeyDown(e);
615
+ e.preventDefault();
616
+ stepOnce((e.key === "ArrowUp" ? 1 : -1) * (e.shiftKey ? 10 : 1));
617
+ };
618
+ const press = (direction) => (e) => {
619
+ if (e.button !== 0 || endHold.current) return;
620
+ e.preventDefault();
621
+ const start = from();
622
+ if (!start) return setInvalid(true);
623
+ const delta = direction * (e.shiftKey ? 10 : 1);
624
+ let amount = stepAmount(start.amount, delta, negative);
625
+ const show = () => {
626
+ setInvalid(false);
627
+ setDraft(amount);
628
+ commit.current(`${amount}${start.unit}`, true);
629
+ };
630
+ show();
631
+ let timer = window.setTimeout(function repeat() {
632
+ amount = stepAmount(amount, delta, negative);
633
+ show();
634
+ timer = window.setTimeout(repeat, REPEAT_EVERY_MS);
635
+ }, REPEAT_AFTER_MS);
636
+ const button = e.currentTarget;
637
+ try {
638
+ button.setPointerCapture(e.pointerId);
639
+ } catch {
640
+ }
641
+ const events = ["pointerup", "pointercancel", "lostpointercapture"];
642
+ const pointerId = e.pointerId;
643
+ const end = () => {
644
+ window.clearTimeout(timer);
645
+ for (const type of events) button.removeEventListener(type, release);
646
+ window.removeEventListener("pointerup", release);
647
+ window.removeEventListener("blur", end);
648
+ endHold.current = null;
649
+ commit.current(`${amount}${start.unit}`);
650
+ };
651
+ const release = (ev) => {
652
+ if (ev.pointerId === void 0 || ev.pointerId === pointerId)
653
+ end();
654
+ };
655
+ endHold.current = end;
656
+ for (const type of events) button.addEventListener(type, release);
657
+ window.addEventListener("pointerup", release);
658
+ window.addEventListener("blur", end);
659
+ };
660
+ const click = (direction) => (e) => {
661
+ if (e.detail === 0) stepOnce(direction);
662
+ };
663
+ const pickUnit = () => {
664
+ endHold.current?.();
665
+ const next = units[(units.indexOf(unit) + 1) % units.length];
666
+ const text = draft.trim();
667
+ if (!BARE_NUMBER.test(text)) return setPicked(next);
668
+ const normal = normaliseLength(text, units, negative, next);
669
+ if (normal === null) return setInvalid(true);
670
+ setPicked(next);
671
+ commit.current(normal);
672
+ };
673
+ const unitName = (u) => u || labels.noUnit;
674
+ const readable = draft.trim() === "" || BARE_NUMBER.test(draft.trim());
675
+ 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";
676
+ return /* @__PURE__ */ jsxs2(
677
+ "div",
678
+ {
679
+ className: cn(
680
+ "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",
681
+ invalid && "border-red-400"
682
+ ),
683
+ children: [
684
+ /* @__PURE__ */ jsx3(
685
+ "input",
686
+ {
687
+ 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",
688
+ value: draft,
689
+ placeholder: fallback ? fallback.amount : placeholder,
690
+ inputMode: "decimal",
691
+ ...fieldLabel,
692
+ "aria-labelledby": labelledBy ?? fieldLabel["aria-labelledby"],
693
+ "aria-invalid": invalid,
694
+ onChange: (e) => {
695
+ endHold.current?.();
696
+ setDraft(e.target.value);
697
+ },
698
+ onBlur: done,
699
+ onKeyDown: onKey
700
+ }
701
+ ),
702
+ readable && (units.length > 1 && !compact ? /* @__PURE__ */ jsx3(
703
+ "button",
704
+ {
705
+ type: "button",
706
+ onClick: pickUnit,
707
+ title: labels.changeUnit(unitName(unit)),
708
+ "aria-label": labels.changeUnit(unitName(unit)),
709
+ 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",
710
+ children: unitName(unit)
711
+ }
712
+ ) : (
713
+ // Narrow, the unit gives its place to the arrows while they show; a unit is typed.
714
+ /* @__PURE__ */ jsx3(
715
+ "span",
716
+ {
717
+ "aria-hidden": true,
718
+ className: cn(
719
+ "shrink-0 px-0.5 font-mono text-[10px] text-panel-muted-fg",
720
+ compact && "group-focus-within/length:hidden group-hover/length:hidden"
721
+ ),
722
+ children: unitName(unit)
723
+ }
724
+ )
725
+ )),
726
+ /* @__PURE__ */ jsxs2(
727
+ "span",
728
+ {
729
+ className: cn(
730
+ "h-full shrink-0 flex-col border-l border-panel-border",
731
+ compact ? "hidden group-focus-within/length:flex group-hover/length:flex" : "flex"
732
+ ),
733
+ children: [
734
+ /* @__PURE__ */ jsx3(
735
+ "button",
736
+ {
737
+ type: "button",
738
+ tabIndex: -1,
739
+ "aria-label": labels.increase,
740
+ onPointerDown: press(1),
741
+ onClick: click(1),
742
+ className: arrow,
743
+ children: /* @__PURE__ */ jsx3(ChevronUp, { className: "size-2.5" })
744
+ }
745
+ ),
746
+ /* @__PURE__ */ jsx3(
747
+ "button",
748
+ {
749
+ type: "button",
750
+ tabIndex: -1,
751
+ "aria-label": labels.decrease,
752
+ onPointerDown: press(-1),
753
+ onClick: click(-1),
754
+ className: arrow,
755
+ children: /* @__PURE__ */ jsx3(ChevronDown, { className: "size-2.5" })
756
+ }
757
+ )
758
+ ]
759
+ }
760
+ )
761
+ ]
762
+ }
763
+ );
764
+ }
765
+ function BoxInput({
766
+ attributes,
767
+ attr,
768
+ placeholder,
769
+ units,
770
+ labels,
771
+ onCommit
772
+ }) {
773
+ const sides = readBox(attributes, attr);
774
+ const defaults = readBox(placeholder ? { [attr]: placeholder } : {}, attr);
775
+ const symmetric = sides[0] === sides[2] && sides[1] === sides[3];
776
+ const [each, setEach] = useState2(!symmetric);
777
+ useEffect2(() => {
778
+ if (!symmetric) setEach(true);
779
+ }, [symmetric]);
780
+ const set = (indices, v, partial) => {
781
+ const next = sides.map((s, i) => s || defaults[i]);
782
+ for (const i of indices) next[i] = v || defaults[i];
783
+ onCommit(next.every((s, i) => s === defaults[i]) ? "" : boxShorthand(next), partial);
784
+ };
785
+ const fieldLabel = useFieldLabel();
786
+ const sideId = useId();
787
+ const sideNames = [labels.sideTop, labels.sideRight, labels.sideBottom, labels.sideLeft];
788
+ const pairs = each ? SIDES.map((_, i) => [sideNames[i], [i]]) : [
789
+ [labels.sidesVertical, [0, 2]],
790
+ [labels.sidesHorizontal, [1, 3]]
791
+ ];
792
+ return /* @__PURE__ */ jsxs2("div", { className: "flex items-start gap-1.5", children: [
793
+ /* @__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: [
794
+ /* @__PURE__ */ jsx3(
795
+ LengthInput,
796
+ {
797
+ value: sides[indices[0]],
798
+ placeholder: defaults[indices[0]] || "0",
799
+ units,
800
+ compact: each,
801
+ labels,
802
+ labelledBy: [fieldLabel["aria-labelledby"], `${sideId}-${indices[0]}`].filter(Boolean).join(" "),
803
+ onCommit: (v, partial) => set(indices, v, partial)
804
+ }
805
+ ),
806
+ /* @__PURE__ */ jsx3(
807
+ "span",
808
+ {
809
+ id: `${sideId}-${indices[0]}`,
810
+ className: "truncate text-center text-[10px] text-panel-muted-fg/80",
811
+ children: name
812
+ }
813
+ )
814
+ ] }, name)) }),
815
+ /* @__PURE__ */ jsx3(
816
+ "button",
817
+ {
818
+ type: "button",
819
+ onClick: () => setEach((v) => !v),
820
+ disabled: each && !symmetric,
821
+ "aria-pressed": each,
822
+ title: labels.editEachSide,
823
+ "aria-label": labels.editEachSide,
824
+ className: cn(
825
+ "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",
826
+ each && "border-brand text-panel-fg"
827
+ ),
828
+ children: /* @__PURE__ */ jsx3(Grid2x2, { className: "size-3.5" })
829
+ }
830
+ )
831
+ ] });
832
+ }
833
+ var ALIGN_ICONS = {
834
+ left: /* @__PURE__ */ jsx3(AlignLeft, { className: "size-3.5" }),
835
+ center: /* @__PURE__ */ jsx3(AlignCenter, { className: "size-3.5" }),
836
+ right: /* @__PURE__ */ jsx3(AlignRight, { className: "size-3.5" }),
837
+ justify: /* @__PURE__ */ jsx3(AlignJustify, { className: "size-3.5" })
838
+ };
839
+ function Segmented({
840
+ value,
841
+ fallback,
842
+ options,
843
+ name,
844
+ onCommit
845
+ }) {
846
+ const current = value || fallback;
847
+ const iconsOnly = options.every((o) => o.value in ALIGN_ICONS);
848
+ return /* @__PURE__ */ jsx3("div", { className: "flex h-7 overflow-hidden rounded-md border border-panel-border bg-panel-elevated", children: options.map((o) => {
849
+ const active = current === o.value;
850
+ return /* @__PURE__ */ jsx3(
851
+ "button",
852
+ {
853
+ type: "button",
854
+ "aria-pressed": active,
855
+ title: name(o),
856
+ "aria-label": name(o),
857
+ onClick: () => o.value !== value && onCommit(o.value),
858
+ className: cn(
859
+ "flex min-w-0 flex-1 items-center justify-center px-1 text-[11px] text-panel-muted-fg transition-colors hover:text-panel-fg",
860
+ active && (value ? "bg-brand text-brand-fg hover:text-brand-fg" : "bg-panel-hover text-panel-fg")
861
+ ),
862
+ children: iconsOnly ? ALIGN_ICONS[o.value] : /* @__PURE__ */ jsx3("span", { className: "truncate", children: name(o) })
863
+ },
864
+ o.value
865
+ );
866
+ }) });
867
+ }
868
+ function Select({
869
+ value,
870
+ fallback,
871
+ options,
872
+ name,
873
+ onCommit
874
+ }) {
875
+ const known = !value || options.some((o) => o.value === value);
876
+ const fallbackOption = options.find((o) => o.value === fallback);
877
+ const field = useFieldLabel();
878
+ return /* @__PURE__ */ jsxs2(
879
+ "select",
880
+ {
881
+ className: cn(FIELD, "px-1.5"),
882
+ ...field,
883
+ value: value || "",
884
+ onChange: (e) => onCommit(e.target.value),
885
+ children: [
886
+ /* @__PURE__ */ jsx3("option", { value: "", children: fallbackOption ? name(fallbackOption) : "\u2014" }),
887
+ !known && /* @__PURE__ */ jsx3("option", { value, children: value }),
888
+ options.map((o) => /* @__PURE__ */ jsx3("option", { value: o.value, children: name(o) }, o.value))
889
+ ]
890
+ }
891
+ );
892
+ }
893
+ function Toggle({
894
+ checked,
895
+ label,
896
+ onCommit
897
+ }) {
898
+ return /* @__PURE__ */ jsx3(
899
+ Switch,
900
+ {
901
+ checked,
902
+ onCheckedChange: onCommit,
903
+ "aria-label": label,
904
+ className: "justify-self-end data-checked:bg-brand data-unchecked:bg-panel-border"
905
+ }
906
+ );
907
+ }
908
+
909
+ // src/components/editor/mjml-attributes.ts
910
+ var ALLOWED_ATTRIBUTES = {
911
+ "mj-body": [
912
+ "background-color",
913
+ "width"
914
+ ],
915
+ "mj-wrapper": [
916
+ "background-color",
917
+ "background-position",
918
+ "background-position-x",
919
+ "background-position-y",
920
+ "background-repeat",
921
+ "background-size",
922
+ "background-url",
923
+ "border",
924
+ "border-bottom",
925
+ "border-left",
926
+ "border-radius",
927
+ "border-right",
928
+ "border-top",
929
+ "direction",
930
+ "full-width",
931
+ "gap",
932
+ "padding",
933
+ "padding-bottom",
934
+ "padding-left",
935
+ "padding-right",
936
+ "padding-top",
937
+ "text-align",
938
+ "text-padding"
939
+ ],
940
+ "mj-section": [
941
+ "background-color",
942
+ "background-position",
943
+ "background-position-x",
944
+ "background-position-y",
945
+ "background-repeat",
946
+ "background-size",
947
+ "background-url",
948
+ "border",
949
+ "border-bottom",
950
+ "border-left",
951
+ "border-radius",
952
+ "border-right",
953
+ "border-top",
954
+ "direction",
955
+ "full-width",
956
+ "padding",
957
+ "padding-bottom",
958
+ "padding-left",
959
+ "padding-right",
960
+ "padding-top",
961
+ "text-align",
962
+ "text-padding"
963
+ ],
964
+ "mj-column": [
965
+ "background-color",
966
+ "border",
967
+ "border-bottom",
968
+ "border-left",
969
+ "border-radius",
970
+ "border-right",
971
+ "border-top",
972
+ "direction",
973
+ "inner-background-color",
974
+ "inner-border",
975
+ "inner-border-bottom",
976
+ "inner-border-left",
977
+ "inner-border-radius",
978
+ "inner-border-right",
979
+ "inner-border-top",
980
+ "padding",
981
+ "padding-bottom",
982
+ "padding-left",
983
+ "padding-right",
984
+ "padding-top",
985
+ "vertical-align",
986
+ "width"
987
+ ],
988
+ "mj-group": [
989
+ "background-color",
990
+ "direction",
991
+ "vertical-align",
992
+ "width"
993
+ ],
994
+ "mj-text": [
995
+ "align",
996
+ "background-color",
997
+ "color",
998
+ "container-background-color",
999
+ "font-family",
1000
+ "font-size",
1001
+ "font-style",
1002
+ "font-weight",
1003
+ "height",
1004
+ "letter-spacing",
1005
+ "line-height",
1006
+ "padding",
1007
+ "padding-bottom",
1008
+ "padding-left",
1009
+ "padding-right",
1010
+ "padding-top",
1011
+ "text-decoration",
1012
+ "text-transform",
1013
+ "vertical-align"
1014
+ ],
1015
+ "mj-button": [
1016
+ "align",
1017
+ "background-color",
1018
+ "border",
1019
+ "border-bottom",
1020
+ "border-left",
1021
+ "border-radius",
1022
+ "border-right",
1023
+ "border-top",
1024
+ "color",
1025
+ "container-background-color",
1026
+ "font-family",
1027
+ "font-size",
1028
+ "font-style",
1029
+ "font-weight",
1030
+ "height",
1031
+ "href",
1032
+ "inner-padding",
1033
+ "letter-spacing",
1034
+ "line-height",
1035
+ "name",
1036
+ "padding",
1037
+ "padding-bottom",
1038
+ "padding-left",
1039
+ "padding-right",
1040
+ "padding-top",
1041
+ "rel",
1042
+ "target",
1043
+ "text-align",
1044
+ "text-decoration",
1045
+ "text-transform",
1046
+ "title",
1047
+ "vertical-align",
1048
+ "width"
1049
+ ],
1050
+ "mj-image": [
1051
+ "align",
1052
+ "alt",
1053
+ "border",
1054
+ "border-bottom",
1055
+ "border-left",
1056
+ "border-radius",
1057
+ "border-right",
1058
+ "border-top",
1059
+ "container-background-color",
1060
+ "fluid-on-mobile",
1061
+ "font-size",
1062
+ "height",
1063
+ "href",
1064
+ "max-height",
1065
+ "name",
1066
+ "padding",
1067
+ "padding-bottom",
1068
+ "padding-left",
1069
+ "padding-right",
1070
+ "padding-top",
1071
+ "rel",
1072
+ "sizes",
1073
+ "src",
1074
+ "srcset",
1075
+ "target",
1076
+ "title",
1077
+ "usemap",
1078
+ "width"
1079
+ ],
1080
+ "mj-divider": [
1081
+ "align",
1082
+ "border-color",
1083
+ "border-style",
1084
+ "border-width",
1085
+ "container-background-color",
1086
+ "padding",
1087
+ "padding-bottom",
1088
+ "padding-left",
1089
+ "padding-right",
1090
+ "padding-top",
1091
+ "width"
1092
+ ],
1093
+ "mj-spacer": [
1094
+ "border",
1095
+ "border-bottom",
1096
+ "border-left",
1097
+ "border-right",
1098
+ "border-top",
1099
+ "container-background-color",
1100
+ "height",
1101
+ "padding",
1102
+ "padding-bottom",
1103
+ "padding-left",
1104
+ "padding-right",
1105
+ "padding-top"
1106
+ ],
1107
+ "mj-social": [
1108
+ "align",
1109
+ "border-radius",
1110
+ "color",
1111
+ "container-background-color",
1112
+ "font-family",
1113
+ "font-size",
1114
+ "font-style",
1115
+ "font-weight",
1116
+ "icon-height",
1117
+ "icon-padding",
1118
+ "icon-size",
1119
+ "inner-padding",
1120
+ "line-height",
1121
+ "mode",
1122
+ "padding",
1123
+ "padding-bottom",
1124
+ "padding-left",
1125
+ "padding-right",
1126
+ "padding-top",
1127
+ "table-layout",
1128
+ "text-decoration",
1129
+ "text-padding",
1130
+ "vertical-align"
1131
+ ],
1132
+ "mj-social-element": [
1133
+ "align",
1134
+ "alt",
1135
+ "background-color",
1136
+ "border-radius",
1137
+ "color",
1138
+ "font-family",
1139
+ "font-size",
1140
+ "font-style",
1141
+ "font-weight",
1142
+ "href",
1143
+ "icon-height",
1144
+ "icon-padding",
1145
+ "icon-position",
1146
+ "icon-size",
1147
+ "line-height",
1148
+ "name",
1149
+ "padding",
1150
+ "padding-bottom",
1151
+ "padding-left",
1152
+ "padding-right",
1153
+ "padding-top",
1154
+ "rel",
1155
+ "sizes",
1156
+ "src",
1157
+ "srcset",
1158
+ "target",
1159
+ "text-decoration",
1160
+ "text-padding",
1161
+ "title",
1162
+ "vertical-align"
1163
+ ],
1164
+ "mj-hero": [
1165
+ "background-color",
1166
+ "background-height",
1167
+ "background-position",
1168
+ "background-url",
1169
+ "background-width",
1170
+ "border-radius",
1171
+ "container-background-color",
1172
+ "height",
1173
+ "inner-background-color",
1174
+ "inner-padding",
1175
+ "inner-padding-bottom",
1176
+ "inner-padding-left",
1177
+ "inner-padding-right",
1178
+ "inner-padding-top",
1179
+ "mode",
1180
+ "padding",
1181
+ "padding-bottom",
1182
+ "padding-left",
1183
+ "padding-right",
1184
+ "padding-top",
1185
+ "vertical-align"
1186
+ ],
1187
+ "mj-navbar": [
1188
+ "align",
1189
+ "base-url",
1190
+ "hamburger",
1191
+ "ico-align",
1192
+ "ico-close",
1193
+ "ico-color",
1194
+ "ico-font-family",
1195
+ "ico-font-size",
1196
+ "ico-line-height",
1197
+ "ico-open",
1198
+ "ico-padding",
1199
+ "ico-padding-bottom",
1200
+ "ico-padding-left",
1201
+ "ico-padding-right",
1202
+ "ico-padding-top",
1203
+ "ico-text-decoration",
1204
+ "ico-text-transform",
1205
+ "padding",
1206
+ "padding-bottom",
1207
+ "padding-left",
1208
+ "padding-right",
1209
+ "padding-top"
1210
+ ],
1211
+ "mj-navbar-link": [
1212
+ "color",
1213
+ "font-family",
1214
+ "font-size",
1215
+ "font-style",
1216
+ "font-weight",
1217
+ "href",
1218
+ "letter-spacing",
1219
+ "line-height",
1220
+ "name",
1221
+ "padding",
1222
+ "padding-bottom",
1223
+ "padding-left",
1224
+ "padding-right",
1225
+ "padding-top",
1226
+ "rel",
1227
+ "target",
1228
+ "text-decoration",
1229
+ "text-transform"
1230
+ ]
1231
+ };
1232
+ var ALWAYS_ALLOWED = /* @__PURE__ */ new Set(["mj-class", "css-class", "id", "style"]);
1233
+ var START_TAG = /<[a-z][^\s/>]*(?:[^>"']|"[^"]*"|'[^']*')*>/gi;
1234
+ var ATTRIBUTE = /\s+([^\s"'>/=]+)(?:\s*=\s*(?:"[^"]*"|'[^']*'|[^\s"'>]+))?/g;
1235
+ function withoutEditorProps(mjml) {
1236
+ return mjml.replace(
1237
+ START_TAG,
1238
+ (tag) => tag.replace(
1239
+ ATTRIBUTE,
1240
+ (attribute, name) => name.toLowerCase().startsWith("data-gjs-") ? "" : attribute
1241
+ )
1242
+ );
1243
+ }
1244
+ function illegalAttributes(tag, attributes) {
1245
+ const allowed = ALLOWED_ATTRIBUTES[tag];
1246
+ if (!allowed) return [];
1247
+ return Object.keys(attributes).filter((a) => !ALWAYS_ALLOWED.has(a) && !allowed.includes(a));
1248
+ }
1249
+ var NAMED_COLORS = /* @__PURE__ */ new Set([
1250
+ "aliceblue",
1251
+ "antiquewhite",
1252
+ "aqua",
1253
+ "aquamarine",
1254
+ "azure",
1255
+ "beige",
1256
+ "bisque",
1257
+ "black",
1258
+ "blanchedalmond",
1259
+ "blue",
1260
+ "blueviolet",
1261
+ "brown",
1262
+ "burlywood",
1263
+ "cadetblue",
1264
+ "chartreuse",
1265
+ "chocolate",
1266
+ "coral",
1267
+ "cornflowerblue",
1268
+ "cornsilk",
1269
+ "crimson",
1270
+ "cyan",
1271
+ "darkblue",
1272
+ "darkcyan",
1273
+ "darkgoldenrod",
1274
+ "darkgray",
1275
+ "darkgreen",
1276
+ "darkgrey",
1277
+ "darkkhaki",
1278
+ "darkmagenta",
1279
+ "darkolivegreen",
1280
+ "darkorange",
1281
+ "darkorchid",
1282
+ "darkred",
1283
+ "darksalmon",
1284
+ "darkseagreen",
1285
+ "darkslateblue",
1286
+ "darkslategray",
1287
+ "darkslategrey",
1288
+ "darkturquoise",
1289
+ "darkviolet",
1290
+ "deeppink",
1291
+ "deepskyblue",
1292
+ "dimgray",
1293
+ "dimgrey",
1294
+ "dodgerblue",
1295
+ "firebrick",
1296
+ "floralwhite",
1297
+ "forestgreen",
1298
+ "fuchsia",
1299
+ "gainsboro",
1300
+ "ghostwhite",
1301
+ "gold",
1302
+ "goldenrod",
1303
+ "gray",
1304
+ "green",
1305
+ "greenyellow",
1306
+ "grey",
1307
+ "honeydew",
1308
+ "hotpink",
1309
+ "indianred",
1310
+ "indigo",
1311
+ "inherit",
1312
+ "ivory",
1313
+ "khaki",
1314
+ "lavender",
1315
+ "lavenderblush",
1316
+ "lawngreen",
1317
+ "lemonchiffon",
1318
+ "lightblue",
1319
+ "lightcoral",
1320
+ "lightcyan",
1321
+ "lightgoldenrodyellow",
1322
+ "lightgray",
1323
+ "lightgreen",
1324
+ "lightgrey",
1325
+ "lightpink",
1326
+ "lightsalmon",
1327
+ "lightseagreen",
1328
+ "lightskyblue",
1329
+ "lightslategray",
1330
+ "lightslategrey",
1331
+ "lightsteelblue",
1332
+ "lightyellow",
1333
+ "lime",
1334
+ "limegreen",
1335
+ "linen",
1336
+ "magenta",
1337
+ "maroon",
1338
+ "mediumaquamarine",
1339
+ "mediumblue",
1340
+ "mediumorchid",
1341
+ "mediumpurple",
1342
+ "mediumseagreen",
1343
+ "mediumslateblue",
1344
+ "mediumspringgreen",
1345
+ "mediumturquoise",
1346
+ "mediumvioletred",
1347
+ "midnightblue",
1348
+ "mintcream",
1349
+ "mistyrose",
1350
+ "moccasin",
1351
+ "navajowhite",
1352
+ "navy",
1353
+ "oldlace",
1354
+ "olive",
1355
+ "olivedrab",
1356
+ "orange",
1357
+ "orangered",
1358
+ "orchid",
1359
+ "palegoldenrod",
1360
+ "palegreen",
1361
+ "paleturquoise",
1362
+ "palevioletred",
1363
+ "papayawhip",
1364
+ "peachpuff",
1365
+ "peru",
1366
+ "pink",
1367
+ "plum",
1368
+ "powderblue",
1369
+ "purple",
1370
+ "rebeccapurple",
1371
+ "red",
1372
+ "rosybrown",
1373
+ "royalblue",
1374
+ "saddlebrown",
1375
+ "salmon",
1376
+ "sandybrown",
1377
+ "seagreen",
1378
+ "seashell",
1379
+ "sienna",
1380
+ "silver",
1381
+ "skyblue",
1382
+ "slateblue",
1383
+ "slategray",
1384
+ "slategrey",
1385
+ "snow",
1386
+ "springgreen",
1387
+ "steelblue",
1388
+ "tan",
1389
+ "teal",
1390
+ "thistle",
1391
+ "tomato",
1392
+ "transparent",
1393
+ "turquoise",
1394
+ "violet",
1395
+ "wheat",
1396
+ "white",
1397
+ "whitesmoke",
1398
+ "yellow",
1399
+ "yellowgreen"
1400
+ ]);
1401
+
1402
+ // src/components/editor/ColorField.tsx
1403
+ import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
361
1404
  var PRESETS = [
362
1405
  "#000000",
363
1406
  "#374151",
@@ -378,32 +1421,33 @@ var eyeDropper = () => globalThis.EyeDropper;
378
1421
  function ColorField({
379
1422
  value,
380
1423
  placeholder,
381
- documentColors,
1424
+ documentColors: documentColors2,
382
1425
  labels,
383
1426
  onChange
384
1427
  }) {
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({
1428
+ const field = useFieldLabel();
1429
+ const [text, setText] = useState3(value);
1430
+ const [open, setOpen] = useState3(false);
1431
+ const [used, setUsed] = useState3([]);
1432
+ const pressed = useRef3(false);
1433
+ const dragged = useRef3(null);
1434
+ const fieldRef = useRef3(null);
1435
+ const anchor = useRef3({
392
1436
  getBoundingClientRect: () => {
393
- const field = fieldRef.current?.getBoundingClientRect() ?? new DOMRect();
1437
+ const field2 = fieldRef.current?.getBoundingClientRect() ?? new DOMRect();
394
1438
  const panel = fieldRef.current?.closest(".editor-dark")?.getBoundingClientRect();
395
- return new DOMRect(panel?.right ?? field.right, field.top, 0, field.height);
1439
+ return new DOMRect(panel?.right ?? field2.right, field2.top, 0, field2.height);
396
1440
  }
397
1441
  });
398
- useEffect2(() => setText(value), [value]);
399
- const latestOnChange = useRef2(onChange);
1442
+ useEffect3(() => setText(value), [value]);
1443
+ const latestOnChange = useRef3(onChange);
400
1444
  latestOnChange.current = onChange;
401
1445
  const release = useCallback2(() => {
402
1446
  pressed.current = false;
403
1447
  if (dragged.current !== null) latestOnChange.current(dragged.current, false);
404
1448
  dragged.current = null;
405
1449
  }, []);
406
- useEffect2(() => {
1450
+ useEffect3(() => {
407
1451
  document.addEventListener("lostpointercapture", release);
408
1452
  return () => {
409
1453
  document.removeEventListener("lostpointercapture", release);
@@ -412,12 +1456,16 @@ function ColorField({
412
1456
  }, [release]);
413
1457
  const hex = toHex(value) ?? toHex(placeholder ?? "") ?? "#000000";
414
1458
  const translucent = hex.length === 9;
415
- const empty = !value;
1459
+ const shown = value || placeholder || "";
416
1460
  const commit = (next) => {
417
1461
  dragged.current = null;
418
- onChange(next, false);
1462
+ const color = mjmlColor(next);
1463
+ if (color === null) return setText(value);
1464
+ setText(color);
1465
+ onChange(color, false);
419
1466
  };
420
- const drag = (next) => {
1467
+ const drag = (picked) => {
1468
+ const next = mjmlColor(picked) ?? picked;
421
1469
  setText(next);
422
1470
  if (!pressed.current) return commit(next);
423
1471
  dragged.current = next;
@@ -432,29 +1480,30 @@ function ColorField({
432
1480
  } catch {
433
1481
  }
434
1482
  };
435
- return /* @__PURE__ */ jsxs2(
1483
+ return /* @__PURE__ */ jsxs3(
436
1484
  Popover,
437
1485
  {
438
1486
  open,
439
1487
  onOpenChange: (next) => {
440
- if (next) setUsed(documentColors());
1488
+ if (next) setUsed(documentColors2());
441
1489
  else if (text.trim() !== value) commit(text.trim());
442
1490
  setOpen(next);
443
1491
  },
444
1492
  children: [
445
- /* @__PURE__ */ jsx3(PopoverAnchor, { virtualRef: anchor }),
446
- /* @__PURE__ */ jsxs2(
1493
+ /* @__PURE__ */ jsx4(PopoverAnchor, { virtualRef: anchor }),
1494
+ /* @__PURE__ */ jsxs3(
447
1495
  "div",
448
1496
  {
449
1497
  ref: fieldRef,
450
1498
  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
1499
  children: [
452
- /* @__PURE__ */ jsx3(
1500
+ /* @__PURE__ */ jsx4(
453
1501
  "input",
454
1502
  {
455
1503
  value: text,
456
1504
  placeholder,
457
1505
  spellCheck: false,
1506
+ ...field,
458
1507
  onChange: (e) => setText(e.target.value),
459
1508
  onBlur: () => text !== value && commit(text.trim()),
460
1509
  onKeyDown: (e) => {
@@ -464,7 +1513,7 @@ function ColorField({
464
1513
  className: "min-w-0 flex-1 bg-transparent font-mono text-xs text-panel-fg outline-none placeholder:text-panel-muted-fg"
465
1514
  }
466
1515
  ),
467
- /* @__PURE__ */ jsx3(PopoverTrigger, { asChild: true, children: /* @__PURE__ */ jsxs2(
1516
+ /* @__PURE__ */ jsx4(PopoverTrigger, { asChild: true, children: /* @__PURE__ */ jsxs3(
468
1517
  "button",
469
1518
  {
470
1519
  type: "button",
@@ -472,15 +1521,15 @@ function ColorField({
472
1521
  "aria-label": labels.colorPickerOpen,
473
1522
  className: "relative size-5 shrink-0 overflow-hidden rounded border border-white/20 outline-none focus-visible:ring-2 focus-visible:ring-brand",
474
1523
  children: [
475
- /* @__PURE__ */ jsx3(Checkerboard, {}),
476
- !empty && /* @__PURE__ */ jsx3("span", { className: "absolute inset-0", style: { background: value } })
1524
+ /* @__PURE__ */ jsx4(Checkerboard, {}),
1525
+ mjmlColor(shown) && /* @__PURE__ */ jsx4("span", { className: "absolute inset-0", style: { background: shown } })
477
1526
  ]
478
1527
  }
479
1528
  ) })
480
1529
  ]
481
1530
  }
482
1531
  ),
483
- /* @__PURE__ */ jsxs2(
1532
+ /* @__PURE__ */ jsxs3(
484
1533
  PopoverContent,
485
1534
  {
486
1535
  side: "right",
@@ -488,36 +1537,37 @@ function ColorField({
488
1537
  sideOffset: 8,
489
1538
  className: "editor-dark flex w-52 flex-col gap-2.5 border border-panel-border bg-panel p-2.5 text-panel-fg",
490
1539
  children: [
491
- /* @__PURE__ */ jsx3(
1540
+ /* @__PURE__ */ jsx4(
492
1541
  "div",
493
1542
  {
494
1543
  onPointerDownCapture: (e) => {
495
1544
  pressed.current = true;
496
1545
  e.target.setPointerCapture(e.pointerId);
497
1546
  },
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" })
1547
+ 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
1548
  }
500
1549
  ),
501
- /* @__PURE__ */ jsxs2("div", { className: "flex items-center gap-1.5", children: [
502
- /* @__PURE__ */ jsx3(
1550
+ /* @__PURE__ */ jsxs3("div", { className: "flex items-center gap-1.5", children: [
1551
+ /* @__PURE__ */ jsx4(
503
1552
  "span",
504
1553
  {
505
1554
  className: "size-6 shrink-0 rounded border border-white/20",
506
1555
  style: { background: hex }
507
1556
  }
508
1557
  ),
509
- /* @__PURE__ */ jsx3(
1558
+ /* @__PURE__ */ jsx4(
510
1559
  "input",
511
1560
  {
512
1561
  value: text,
513
1562
  spellCheck: false,
1563
+ ...field,
514
1564
  onChange: (e) => setText(e.target.value),
515
1565
  onKeyDown: (e) => e.key === "Enter" && commit(text.trim()),
516
1566
  onBlur: () => text !== value && commit(text.trim()),
517
1567
  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
1568
  }
519
1569
  ),
520
- eyeDropper() && /* @__PURE__ */ jsx3(
1570
+ eyeDropper() && /* @__PURE__ */ jsx4(
521
1571
  "button",
522
1572
  {
523
1573
  type: "button",
@@ -525,11 +1575,11 @@ function ColorField({
525
1575
  title: labels.colorPickerEyedropper,
526
1576
  "aria-label": labels.colorPickerEyedropper,
527
1577
  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" })
1578
+ children: /* @__PURE__ */ jsx4(Pipette, { className: "size-3" })
529
1579
  }
530
1580
  )
531
1581
  ] }),
532
- used.length > 0 && /* @__PURE__ */ jsx3(
1582
+ used.length > 0 && /* @__PURE__ */ jsx4(
533
1583
  Swatches,
534
1584
  {
535
1585
  title: labels.colorPickerDocument,
@@ -538,7 +1588,7 @@ function ColorField({
538
1588
  onPick: commit
539
1589
  }
540
1590
  ),
541
- /* @__PURE__ */ jsx3(
1591
+ /* @__PURE__ */ jsx4(
542
1592
  Swatches,
543
1593
  {
544
1594
  title: labels.colorPickerPresets,
@@ -562,10 +1612,10 @@ function Swatches({
562
1612
  onPick,
563
1613
  transparentLabel
564
1614
  }) {
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(
1615
+ return /* @__PURE__ */ jsxs3("div", { children: [
1616
+ /* @__PURE__ */ jsx4("p", { className: "mb-1 text-[10px] font-medium tracking-wide text-panel-muted-fg uppercase", children: title }),
1617
+ /* @__PURE__ */ jsxs3("div", { className: "grid grid-cols-8 gap-1", children: [
1618
+ colors.map((color) => /* @__PURE__ */ jsx4(
569
1619
  "button",
570
1620
  {
571
1621
  type: "button",
@@ -579,21 +1629,21 @@ function Swatches({
579
1629
  },
580
1630
  color
581
1631
  )),
582
- transparentLabel && /* @__PURE__ */ jsx3(
1632
+ transparentLabel && /* @__PURE__ */ jsx4(
583
1633
  "button",
584
1634
  {
585
1635
  type: "button",
586
1636
  title: transparentLabel,
587
1637
  onClick: () => onPick("transparent"),
588
1638
  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, {})
1639
+ children: /* @__PURE__ */ jsx4(Checkerboard, {})
590
1640
  }
591
1641
  )
592
1642
  ] })
593
1643
  ] });
594
1644
  }
595
1645
  function Checkerboard() {
596
- return /* @__PURE__ */ jsx3(
1646
+ return /* @__PURE__ */ jsx4(
597
1647
  "span",
598
1648
  {
599
1649
  className: "absolute inset-0",
@@ -604,6 +1654,16 @@ function Checkerboard() {
604
1654
  }
605
1655
  );
606
1656
  }
1657
+ function mjmlColor(value) {
1658
+ const v = value.trim();
1659
+ if (v === "") return "";
1660
+ if (NAMED_COLORS.has(v.toLowerCase())) return v.toLowerCase();
1661
+ const hex = toHex(v);
1662
+ if (!hex) return null;
1663
+ if (hex.length === 7) return hex;
1664
+ const [r, g, b, a] = [1, 3, 5, 7].map((i) => parseInt(hex.slice(i, i + 2), 16));
1665
+ return `rgba(${r},${g},${b},${Number((a / 255).toFixed(2))})`;
1666
+ }
607
1667
  function toHex(value, { opaque = false } = {}) {
608
1668
  const v = value.trim().toLowerCase();
609
1669
  const short = /^#([0-9a-f])([0-9a-f])([0-9a-f])$/.exec(v);
@@ -623,7 +1683,7 @@ function toHex(value, { opaque = false } = {}) {
623
1683
 
624
1684
  // src/components/editor/ImageField.tsx
625
1685
  import { ImageIcon, X } from "lucide-react";
626
- import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
1686
+ import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
627
1687
  function ImageField({
628
1688
  value,
629
1689
  labels,
@@ -631,8 +1691,8 @@ function ImageField({
631
1691
  onClear
632
1692
  }) {
633
1693
  const url = imageUrl(value);
634
- return /* @__PURE__ */ jsxs3("div", { className: "relative", children: [
635
- /* @__PURE__ */ jsxs3(
1694
+ return /* @__PURE__ */ jsxs4("div", { className: "relative", children: [
1695
+ /* @__PURE__ */ jsxs4(
636
1696
  "button",
637
1697
  {
638
1698
  type: "button",
@@ -640,15 +1700,15 @@ function ImageField({
640
1700
  title: labels.chooseImage,
641
1701
  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
1702
  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" }),
1703
+ 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: [
1704
+ /* @__PURE__ */ jsx5(ImageIcon, { className: "size-5" }),
645
1705
  labels.chooseImage
646
1706
  ] }),
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 })
1707
+ 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
1708
  ]
649
1709
  }
650
1710
  ),
651
- value && /* @__PURE__ */ jsx4(
1711
+ value && /* @__PURE__ */ jsx5(
652
1712
  "button",
653
1713
  {
654
1714
  type: "button",
@@ -656,7 +1716,7 @@ function ImageField({
656
1716
  title: labels.imageFieldRemove,
657
1717
  "aria-label": labels.imageFieldRemove,
658
1718
  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" })
1719
+ children: /* @__PURE__ */ jsx5(X, { className: "size-3" })
660
1720
  }
661
1721
  )
662
1722
  ] });
@@ -669,75 +1729,1280 @@ function imageUrl(value) {
669
1729
  return /^[a-z-]+\(/i.test(v) ? null : v;
670
1730
  }
671
1731
 
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"
1732
+ // src/components/editor/inspector/schema.ts
1733
+ var PX = ["px"];
1734
+ var PX_PCT = ["px", "%"];
1735
+ var LINE_HEIGHT = ["", "px", "%"];
1736
+ var LETTER_SPACING = ["px", "em"];
1737
+ var ALIGN = [
1738
+ { value: "left", label: "alignLeft" },
1739
+ { value: "center", label: "alignCenter" },
1740
+ { value: "right", label: "alignRight" }
1741
+ ];
1742
+ var VERTICAL_ALIGN = [
1743
+ { value: "top", label: "alignTop" },
1744
+ { value: "middle", label: "alignMiddle" },
1745
+ { value: "bottom", label: "alignBottom" }
1746
+ ];
1747
+ var FONTS = [
1748
+ { value: "Ubuntu, Helvetica, Arial, sans-serif", label: "Ubuntu" },
1749
+ { value: "Arial, Helvetica, sans-serif", label: "Arial" },
1750
+ { value: "Helvetica, Arial, sans-serif", label: "Helvetica" },
1751
+ { value: "Verdana, Geneva, sans-serif", label: "Verdana" },
1752
+ { value: "Tahoma, Geneva, sans-serif", label: "Tahoma" },
1753
+ { value: "'Trebuchet MS', Helvetica, sans-serif", label: "Trebuchet MS" },
1754
+ { value: "Georgia, 'Times New Roman', serif", label: "Georgia" },
1755
+ { value: "'Times New Roman', Times, serif", label: "Times New Roman" },
1756
+ { value: "'Courier New', Courier, monospace", label: "Courier New" }
1757
+ ];
1758
+ var WEIGHTS = [
1759
+ { value: "normal", label: "weightRegular" },
1760
+ { value: "500", label: "weightMedium" },
1761
+ { value: "600", label: "weightSemibold" },
1762
+ { value: "bold", label: "weightBold" }
1763
+ ];
1764
+ var TEXT_TRANSFORM = [
1765
+ { value: "none", label: "caseNone" },
1766
+ { value: "uppercase", label: "caseUpper" },
1767
+ { value: "lowercase", label: "caseLower" },
1768
+ { value: "capitalize", label: "caseCapitalize" }
1769
+ ];
1770
+ var TARGET = [
1771
+ { value: "_blank", label: "targetNewTab" },
1772
+ { value: "_self", label: "targetSameTab" }
1773
+ ];
1774
+ var BACKGROUND_POSITION = [
1775
+ { value: "top left", label: "positionTopLeft" },
1776
+ { value: "top center", label: "positionTopCenter" },
1777
+ { value: "top right", label: "positionTopRight" },
1778
+ { value: "center left", label: "positionCenterLeft" },
1779
+ { value: "center center", label: "positionCenter" },
1780
+ { value: "center right", label: "positionCenterRight" },
1781
+ { value: "bottom left", label: "positionBottomLeft" },
1782
+ { value: "bottom center", label: "positionBottomCenter" },
1783
+ { value: "bottom right", label: "positionBottomRight" }
1784
+ ];
1785
+ var NETWORKS = [
1786
+ { value: "facebook", label: "Facebook" },
1787
+ { value: "instagram", label: "Instagram" },
1788
+ { value: "x", label: "X" },
1789
+ { value: "linkedin", label: "LinkedIn" },
1790
+ { value: "youtube", label: "YouTube" },
1791
+ { value: "pinterest", label: "Pinterest" },
1792
+ { value: "github", label: "GitHub" },
1793
+ { value: "web", label: "networkWebsite" },
1794
+ { value: "snapchat", label: "Snapchat" },
1795
+ { value: "vimeo", label: "Vimeo" },
1796
+ { value: "medium", label: "Medium" },
1797
+ { value: "dribbble", label: "Dribbble" },
1798
+ { value: "soundcloud", label: "SoundCloud" },
1799
+ { value: "tumblr", label: "Tumblr" },
1800
+ { value: "xing", label: "Xing" },
1801
+ { value: "custom", label: "networkCustom" }
1802
+ ];
1803
+ var hasBackgroundImage = (a) => !!a["background-url"];
1804
+ var SECTION_FIELDS = [
1805
+ {
1806
+ attr: "full-width",
1807
+ label: "fullWidth",
1808
+ group: "layout",
1809
+ control: { type: "toggle", on: "full-width" }
1810
+ },
1811
+ {
1812
+ attr: "padding",
1813
+ label: "padding",
1814
+ group: "spacing",
1815
+ control: { type: "box", units: PX_PCT },
1816
+ default: "20px 0"
1817
+ },
1818
+ {
1819
+ attr: "background-color",
1820
+ label: "backgroundColor",
1821
+ group: "style",
1822
+ control: { type: "color" }
1823
+ },
1824
+ { attr: "background-url", label: "backgroundImage", group: "style", control: { type: "image" } },
1825
+ {
1826
+ attr: "background-size",
1827
+ label: "backgroundSize",
1828
+ group: "style",
1829
+ control: {
1830
+ type: "segmented",
1831
+ options: [
1832
+ { value: "cover", label: "sizeCover" },
1833
+ { value: "contain", label: "sizeContain" },
1834
+ { value: "auto", label: "sizeAuto" }
1835
+ ]
1836
+ },
1837
+ default: "auto",
1838
+ when: hasBackgroundImage
1839
+ },
1840
+ {
1841
+ attr: "background-repeat",
1842
+ label: "backgroundRepeat",
1843
+ group: "style",
1844
+ control: {
1845
+ type: "segmented",
1846
+ options: [
1847
+ { value: "repeat", label: "repeatTile" },
1848
+ { value: "no-repeat", label: "repeatOnce" }
1849
+ ]
1850
+ },
1851
+ default: "repeat",
1852
+ when: hasBackgroundImage
1853
+ },
1854
+ {
1855
+ attr: "background-position",
1856
+ label: "backgroundPosition",
1857
+ group: "style",
1858
+ control: { type: "select", options: BACKGROUND_POSITION },
1859
+ default: "top center",
1860
+ when: hasBackgroundImage
1861
+ },
1862
+ {
1863
+ attr: "border-radius",
1864
+ label: "cornerRadius",
1865
+ group: "style",
1866
+ control: { type: "length", units: PX_PCT }
1867
+ }
1868
+ ];
1869
+ var SCHEMA = {
1870
+ "mj-body": [
1871
+ { attr: "width", label: "contentWidth", group: "layout", control: { type: "length", units: PX }, default: "600px" },
1872
+ { attr: "background-color", label: "pageBackground", group: "style", control: { type: "color" } }
1873
+ ],
1874
+ "mj-wrapper": [
1875
+ ...SECTION_FIELDS,
1876
+ { attr: "gap", label: "gap", group: "advanced", control: { type: "length", units: PX } },
1877
+ { attr: "border", label: "border", group: "advanced", control: { type: "text", placeholder: "1px solid #cccccc" } }
1878
+ ],
1879
+ "mj-section": [
1880
+ ...SECTION_FIELDS,
1881
+ { attr: "text-align", label: "alignColumns", group: "advanced", control: { type: "segmented", options: ALIGN }, default: "center" },
1882
+ { attr: "direction", label: "reverseColumns", group: "advanced", control: { type: "toggle", on: "rtl" } },
1883
+ { attr: "border", label: "border", group: "advanced", control: { type: "text", placeholder: "1px solid #cccccc" } }
1884
+ ],
1885
+ "mj-column": [
1886
+ { attr: "vertical-align", label: "verticalAlign", group: "layout", control: { type: "segmented", options: VERTICAL_ALIGN }, default: "top" },
1887
+ { attr: "padding", label: "padding", group: "spacing", control: { type: "box", units: PX_PCT } },
1888
+ { attr: "background-color", label: "backgroundColor", group: "style", control: { type: "color" } },
1889
+ { attr: "border-radius", label: "cornerRadius", group: "style", control: { type: "length", units: PX_PCT } },
1890
+ { attr: "width", label: "width", group: "advanced", control: { type: "length", units: PX_PCT } },
1891
+ { attr: "inner-background-color", label: "innerBackground", group: "advanced", control: { type: "color" } },
1892
+ { attr: "inner-border-radius", label: "innerCornerRadius", group: "advanced", control: { type: "length", units: PX_PCT } },
1893
+ { attr: "border", label: "border", group: "advanced", control: { type: "text", placeholder: "1px solid #cccccc" } },
1894
+ { attr: "inner-border", label: "innerBorder", group: "advanced", control: { type: "text", placeholder: "1px solid #cccccc" } }
1895
+ ],
1896
+ "mj-text": [
1897
+ {
1898
+ attr: "align",
1899
+ label: "align",
1900
+ group: "layout",
1901
+ control: { type: "segmented", options: [...ALIGN, { value: "justify", label: "alignJustify" }] },
1902
+ default: "left"
1903
+ },
1904
+ { attr: "padding", label: "padding", group: "spacing", control: { type: "box", units: PX_PCT }, default: "10px 25px" },
1905
+ { attr: "font-family", label: "font", group: "style", control: { type: "select", options: FONTS }, default: "Ubuntu, Helvetica, Arial, sans-serif" },
1906
+ { attr: "font-size", label: "fontSize", group: "style", control: { type: "length", units: PX }, default: "13px" },
1907
+ { attr: "font-weight", label: "fontWeight", group: "style", control: { type: "select", options: WEIGHTS } },
1908
+ { attr: "color", label: "textColor", group: "style", control: { type: "color" }, default: "#000000" },
1909
+ { attr: "line-height", label: "lineHeight", group: "style", control: { type: "length", units: LINE_HEIGHT }, default: "1" },
1910
+ { attr: "container-background-color", label: "blockBackground", group: "style", control: { type: "color" } },
1911
+ { attr: "letter-spacing", label: "letterSpacing", group: "advanced", control: { type: "length", units: LETTER_SPACING, negative: true } },
1912
+ { attr: "text-transform", label: "textTransform", group: "advanced", control: { type: "select", options: TEXT_TRANSFORM } }
1913
+ ],
1914
+ "mj-button": [
1915
+ { attr: "href", label: "link", group: "content", control: { type: "link" } },
1916
+ { attr: "align", label: "align", group: "layout", control: { type: "segmented", options: ALIGN }, default: "center" },
1917
+ { attr: "inner-padding", label: "buttonPadding", group: "spacing", control: { type: "box", units: PX_PCT }, default: "10px 25px" },
1918
+ { attr: "padding", label: "spaceAround", group: "spacing", control: { type: "box", units: PX_PCT }, default: "10px 25px" },
1919
+ { attr: "background-color", label: "buttonColor", group: "style", control: { type: "color" }, default: "#414141" },
1920
+ { attr: "color", label: "textColor", group: "style", control: { type: "color" }, default: "#ffffff" },
1921
+ { attr: "font-size", label: "fontSize", group: "style", control: { type: "length", units: PX }, default: "13px" },
1922
+ { attr: "border-radius", label: "cornerRadius", group: "style", control: { type: "length", units: PX_PCT }, default: "3px" },
1923
+ { attr: "width", label: "width", group: "advanced", control: { type: "length", units: PX_PCT } },
1924
+ { attr: "font-family", label: "font", group: "advanced", control: { type: "select", options: FONTS }, default: "Ubuntu, Helvetica, Arial, sans-serif" },
1925
+ { attr: "font-weight", label: "fontWeight", group: "advanced", control: { type: "select", options: WEIGHTS }, default: "normal" },
1926
+ { attr: "target", label: "opensIn", group: "advanced", control: { type: "select", options: TARGET }, default: "_blank" },
1927
+ { attr: "title", label: "tooltip", group: "advanced", control: { type: "text" } },
1928
+ { attr: "rel", label: "rel", group: "advanced", control: { type: "text", placeholder: "noopener" } },
1929
+ { attr: "container-background-color", label: "blockBackground", group: "advanced", control: { type: "color" } },
1930
+ { attr: "border", label: "border", group: "advanced", control: { type: "text", placeholder: "1px solid #cccccc" }, default: "none" },
1931
+ { attr: "letter-spacing", label: "letterSpacing", group: "advanced", control: { type: "length", units: LETTER_SPACING, negative: true } },
1932
+ { attr: "text-transform", label: "textTransform", group: "advanced", control: { type: "select", options: TEXT_TRANSFORM }, default: "none" },
1933
+ { attr: "line-height", label: "lineHeight", group: "advanced", control: { type: "length", units: LINE_HEIGHT }, default: "120%" }
1934
+ ],
1935
+ "mj-image": [
1936
+ { attr: "src", label: "image", group: "content", control: { type: "image" } },
1937
+ { attr: "alt", label: "altText", group: "content", control: { type: "text" } },
1938
+ { attr: "href", label: "link", group: "content", control: { type: "link" } },
1939
+ { attr: "width", label: "width", group: "layout", control: { type: "length", units: PX } },
1940
+ { attr: "align", label: "align", group: "layout", control: { type: "segmented", options: ALIGN }, default: "center" },
1941
+ { attr: "fluid-on-mobile", label: "fluidOnMobile", group: "layout", control: { type: "toggle", on: "true" } },
1942
+ { attr: "padding", label: "padding", group: "spacing", control: { type: "box", units: PX_PCT }, default: "10px 25px" },
1943
+ { attr: "border-radius", label: "cornerRadius", group: "style", control: { type: "length", units: PX_PCT } },
1944
+ { attr: "target", label: "opensIn", group: "advanced", control: { type: "select", options: TARGET }, default: "_blank" },
1945
+ { attr: "title", label: "tooltip", group: "advanced", control: { type: "text" } },
1946
+ { attr: "rel", label: "rel", group: "advanced", control: { type: "text", placeholder: "noopener" } },
1947
+ { attr: "border", label: "border", group: "advanced", control: { type: "text", placeholder: "1px solid #cccccc" }, default: "0" },
1948
+ { attr: "container-background-color", label: "blockBackground", group: "advanced", control: { type: "color" } }
1949
+ ],
1950
+ "mj-divider": [
1951
+ { attr: "width", label: "width", group: "layout", control: { type: "length", units: PX_PCT }, default: "100%" },
1952
+ { attr: "align", label: "align", group: "layout", control: { type: "segmented", options: ALIGN }, default: "center" },
1953
+ { attr: "padding", label: "padding", group: "spacing", control: { type: "box", units: PX_PCT }, default: "10px 25px" },
1954
+ { attr: "border-color", label: "lineColor", group: "style", control: { type: "color" }, default: "#000000" },
1955
+ { attr: "border-width", label: "thickness", group: "style", control: { type: "length", units: PX }, default: "4px" },
1956
+ {
1957
+ attr: "border-style",
1958
+ label: "lineStyle",
1959
+ group: "style",
1960
+ control: {
1961
+ type: "segmented",
1962
+ options: [
1963
+ { value: "solid", label: "lineSolid" },
1964
+ { value: "dashed", label: "lineDashed" },
1965
+ { value: "dotted", label: "lineDotted" }
1966
+ ]
1967
+ },
1968
+ default: "solid"
1969
+ },
1970
+ { attr: "container-background-color", label: "blockBackground", group: "advanced", control: { type: "color" } }
1971
+ ],
1972
+ "mj-spacer": [
1973
+ { attr: "height", label: "height", group: "layout", control: { type: "length", units: PX }, default: "20px" },
1974
+ { attr: "container-background-color", label: "blockBackground", group: "advanced", control: { type: "color" } }
1975
+ ],
1976
+ "mj-social": [
1977
+ {
1978
+ attr: "mode",
1979
+ label: "socialMode",
1980
+ group: "layout",
1981
+ control: {
1982
+ type: "segmented",
1983
+ options: [
1984
+ { value: "horizontal", label: "modeHorizontal" },
1985
+ { value: "vertical", label: "modeVertical" }
1986
+ ]
1987
+ },
1988
+ default: "horizontal"
1989
+ },
1990
+ { attr: "align", label: "align", group: "layout", control: { type: "segmented", options: ALIGN }, default: "center" },
1991
+ { attr: "padding", label: "padding", group: "spacing", control: { type: "box", units: PX_PCT }, default: "10px 25px" },
1992
+ { attr: "inner-padding", label: "iconSpacing", group: "spacing", control: { type: "box", units: PX_PCT } },
1993
+ { attr: "icon-size", label: "iconSize", group: "style", control: { type: "length", units: PX_PCT }, default: "20px" },
1994
+ // A single value: mj-social's border-radius is not a 4-side shorthand.
1995
+ { attr: "border-radius", label: "cornerRadius", group: "style", control: { type: "length", units: PX_PCT }, default: "3px" },
1996
+ { attr: "font-family", label: "font", group: "advanced", control: { type: "select", options: FONTS }, default: "Ubuntu, Helvetica, Arial, sans-serif" },
1997
+ { attr: "font-size", label: "fontSize", group: "advanced", control: { type: "length", units: PX }, default: "13px" },
1998
+ { attr: "color", label: "textColor", group: "advanced", control: { type: "color" }, default: "#333333" },
1999
+ { attr: "line-height", label: "lineHeight", group: "advanced", control: { type: "length", units: LINE_HEIGHT }, default: "22px" },
2000
+ { attr: "container-background-color", label: "blockBackground", group: "advanced", control: { type: "color" } }
2001
+ ],
2002
+ "mj-social-element": [
2003
+ { attr: "name", label: "network", group: "content", control: { type: "network", networks: NETWORKS } },
2004
+ { attr: "href", label: "link", group: "content", control: { type: "link" } },
2005
+ {
2006
+ attr: "src",
2007
+ label: "customIcon",
2008
+ group: "content",
2009
+ control: { type: "image" },
2010
+ when: (a) => (a.name ?? "").startsWith("custom") || !!a.src
2011
+ },
2012
+ { attr: "alt", label: "altText", group: "content", control: { type: "text" } },
2013
+ { attr: "background-color", label: "iconColor", group: "style", control: { type: "color" } },
2014
+ { attr: "icon-size", label: "iconSize", group: "advanced", control: { type: "length", units: PX_PCT } },
2015
+ { attr: "border-radius", label: "cornerRadius", group: "advanced", control: { type: "length", units: PX }, default: "3px" },
2016
+ { attr: "padding", label: "padding", group: "advanced", control: { type: "box", units: PX_PCT }, default: "4px" },
2017
+ { attr: "icon-position", label: "iconPosition", group: "advanced", control: { type: "segmented", options: [{ value: "left", label: "alignLeft" }, { value: "right", label: "alignRight" }] }, default: "left" },
2018
+ { attr: "title", label: "tooltip", group: "advanced", control: { type: "text" } },
2019
+ { attr: "target", label: "opensIn", group: "advanced", control: { type: "select", options: TARGET }, default: "_blank" },
2020
+ { attr: "rel", label: "rel", group: "advanced", control: { type: "text", placeholder: "noopener" } }
2021
+ ],
2022
+ "mj-hero": [
2023
+ { attr: "background-url", label: "backgroundImage", group: "content", control: { type: "image" } },
2024
+ {
2025
+ attr: "mode",
2026
+ label: "heroMode",
2027
+ group: "content",
2028
+ control: {
2029
+ type: "segmented",
2030
+ options: [
2031
+ { value: "fixed-height", label: "heroFixed" },
2032
+ { value: "fluid-height", label: "heroFluid" }
2033
+ ]
2034
+ },
2035
+ default: "fixed-height"
2036
+ },
2037
+ {
2038
+ attr: "height",
2039
+ label: "height",
2040
+ group: "layout",
2041
+ control: { type: "length", units: PX_PCT },
2042
+ default: "0px",
2043
+ when: (a) => (a.mode ?? "fixed-height") === "fixed-height"
2044
+ },
2045
+ {
2046
+ attr: "background-width",
2047
+ label: "backgroundWidth",
2048
+ group: "layout",
2049
+ control: { type: "length", units: PX_PCT },
2050
+ when: (a) => a.mode === "fluid-height"
2051
+ },
2052
+ {
2053
+ attr: "background-height",
2054
+ label: "backgroundHeight",
2055
+ group: "layout",
2056
+ control: { type: "length", units: PX_PCT },
2057
+ when: (a) => a.mode === "fluid-height"
2058
+ },
2059
+ { attr: "vertical-align", label: "verticalAlign", group: "layout", control: { type: "segmented", options: VERTICAL_ALIGN }, default: "top" },
2060
+ { attr: "background-position", label: "backgroundPosition", group: "layout", control: { type: "select", options: BACKGROUND_POSITION }, default: "center center" },
2061
+ { attr: "padding", label: "padding", group: "spacing", control: { type: "box", units: PX_PCT }, default: "0px" },
2062
+ { attr: "background-color", label: "fallbackColor", group: "style", control: { type: "color" }, default: "#ffffff" },
2063
+ { attr: "border-radius", label: "cornerRadius", group: "style", control: { type: "length", units: PX_PCT } },
2064
+ { attr: "inner-padding", label: "contentPadding", group: "advanced", control: { type: "box", units: PX_PCT } },
2065
+ { attr: "inner-background-color", label: "contentBackground", group: "advanced", control: { type: "color" } }
2066
+ ],
2067
+ "mj-navbar": [
2068
+ { attr: "align", label: "align", group: "layout", control: { type: "segmented", options: ALIGN }, default: "center" },
2069
+ { attr: "hamburger", label: "hamburger", group: "layout", control: { type: "toggle", on: "hamburger" } },
2070
+ { attr: "padding", label: "padding", group: "spacing", control: { type: "box", units: PX_PCT } },
2071
+ { attr: "ico-color", label: "menuIconColor", group: "advanced", control: { type: "color" }, default: "#000000", when: (a) => !!a.hamburger },
2072
+ { attr: "ico-font-size", label: "menuIconSize", group: "advanced", control: { type: "length", units: PX_PCT }, default: "30px", when: (a) => !!a.hamburger },
2073
+ { attr: "ico-align", label: "menuIconAlign", group: "advanced", control: { type: "segmented", options: ALIGN }, default: "center", when: (a) => !!a.hamburger },
2074
+ { attr: "base-url", label: "baseUrl", group: "advanced", control: { type: "link" } }
2075
+ ],
2076
+ "mj-navbar-link": [
2077
+ { attr: "href", label: "link", group: "content", control: { type: "link" } },
2078
+ { attr: "padding", label: "padding", group: "spacing", control: { type: "box", units: PX_PCT }, default: "15px 10px" },
2079
+ { attr: "color", label: "textColor", group: "style", control: { type: "color" }, default: "#000000" },
2080
+ { attr: "font-family", label: "font", group: "style", control: { type: "select", options: FONTS }, default: "Ubuntu, Helvetica, Arial, sans-serif" },
2081
+ { attr: "font-size", label: "fontSize", group: "style", control: { type: "length", units: PX }, default: "13px" },
2082
+ { attr: "font-weight", label: "fontWeight", group: "style", control: { type: "select", options: WEIGHTS }, default: "normal" },
2083
+ { attr: "text-transform", label: "textTransform", group: "style", control: { type: "select", options: TEXT_TRANSFORM }, default: "uppercase" },
2084
+ { attr: "target", label: "opensIn", group: "advanced", control: { type: "select", options: TARGET }, default: "_blank" },
2085
+ { attr: "rel", label: "rel", group: "advanced", control: { type: "text", placeholder: "noopener" } },
2086
+ { attr: "letter-spacing", label: "letterSpacing", group: "advanced", control: { type: "length", units: LETTER_SPACING, negative: true } },
2087
+ { attr: "line-height", label: "lineHeight", group: "advanced", control: { type: "length", units: LINE_HEIGHT }, default: "22px" }
2088
+ ]
2089
+ };
2090
+
2091
+ // src/components/editor/layers.ts
2092
+ var svg = (paths) => `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">${paths}</svg>`;
2093
+ var ICONS = {
2094
+ "mj-wrapper": svg(
2095
+ '<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"/>'
2096
+ ),
2097
+ "mj-section": svg('<rect width="20" height="12" x="2" y="6" rx="2"/>'),
2098
+ "mj-hero": svg('<rect width="18" height="18" x="3" y="3" rx="2"/><path d="M3 9h18"/>'),
2099
+ "mj-group": svg(
2100
+ '<rect width="18" height="18" x="3" y="3" rx="2"/><path d="M9 3v18"/><path d="M15 3v18"/>'
2101
+ ),
2102
+ "mj-column": svg('<rect width="18" height="18" x="3" y="3" rx="2"/><path d="M12 3v18"/>'),
2103
+ "mj-text": svg('<path d="M4 7V4h16v3"/><path d="M9 20h6"/><path d="M12 4v16"/>'),
2104
+ "mj-image": svg(
2105
+ '<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"/>'
2106
+ ),
2107
+ "mj-button": svg('<rect width="20" height="10" x="2" y="7" rx="3"/><path d="M8 12h8"/>'),
2108
+ "mj-divider": svg('<path d="M5 12h14"/>'),
2109
+ "mj-spacer": svg('<path d="M12 3v18"/><path d="m8 17 4 4 4-4"/><path d="m8 7 4-4 4 4"/>'),
2110
+ "mj-social": svg(
2111
+ '<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"/>'
2112
+ ),
2113
+ "mj-social-element": svg(
2114
+ '<circle cx="12" cy="12" r="4"/><path d="M16 8v5a3 3 0 0 0 6 0v-1a10 10 0 1 0-4 8"/>'
2115
+ ),
2116
+ "mj-navbar": svg('<path d="M4 6h16"/><path d="M4 12h16"/><path d="M4 18h16"/>'),
2117
+ "mj-navbar-link": svg(
2118
+ '<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"/>'
2119
+ ),
2120
+ "mj-raw": svg('<path d="m16 18 6-6-6-6"/><path d="m8 6-6 6 6 6"/>')
682
2121
  };
683
2122
  function tagOf(c) {
684
2123
  return String(c.get("tagName") || c.get("type") || "");
685
2124
  }
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";
690
- }
691
- function isCommentable(c) {
692
- const t = tagOf(c);
693
- return t === "mj-section" || t in TYPE_LABEL;
694
- }
695
- function classMatch(c, re) {
696
- const cssClass = String(c.getAttributes()["css-class"] ?? "");
697
- return cssClass.match(re)?.[1] ?? null;
698
- }
699
- function sectionIdOf(c) {
700
- return classMatch(c, SEC_ID_RE);
2125
+ function textOf(c) {
2126
+ const template = document.createElement("template");
2127
+ template.innerHTML = c.getInnerHTML();
2128
+ template.content.querySelectorAll("br, p, div, li, tr, h1, h2, h3, h4, h5, h6").forEach((el) => el.after("\n"));
2129
+ const lines = (template.content.textContent ?? "").split("\n");
2130
+ const first = lines.map((l) => l.replace(/\s+/g, " ").trim()).find(Boolean) ?? "";
2131
+ return first.slice(0, 60);
701
2132
  }
702
- function addIdClass(c, prefix) {
703
- const existing = classMatch(c, prefix === "sec" ? SEC_ID_RE : OBJ_ID_RE);
704
- if (existing) return existing;
705
- const cssClass = String(c.getAttributes()["css-class"] ?? "");
706
- const id = Math.random().toString(36).slice(2, 10);
707
- c.addAttributes({ "css-class": `${cssClass} ${prefix}-${id}`.trim() });
708
- return id;
2133
+ function escapeHtml(s) {
2134
+ return s.replace(/[&<>"']/g, (ch) => `&#${ch.charCodeAt(0)};`);
709
2135
  }
710
- function closestSection(c) {
711
- let cur = c;
712
- while (cur) {
713
- if (isSection(cur)) return cur;
714
- cur = cur.parent() ?? void 0;
2136
+ function layerName(c, labels) {
2137
+ const tag = tagOf(c);
2138
+ const kind = labels.elementNames[tag] ?? tag.replace(/^mj-/, "");
2139
+ const attributes = c.getAttributes();
2140
+ switch (tag) {
2141
+ case "mj-text":
2142
+ case "mj-button":
2143
+ case "mj-navbar-link":
2144
+ return textOf(c) || kind;
2145
+ case "mj-image":
2146
+ return attributes.alt?.trim() || kind;
2147
+ case "mj-social-element": {
2148
+ const { network } = parseNetwork(String(attributes.name ?? ""));
2149
+ const option = NETWORKS.find((o) => o.value === network && network !== "custom");
2150
+ return textOf(c) || option && (labels.inspectorLabels[option.label] ?? option.label) || kind;
2151
+ }
2152
+ case "mj-section":
2153
+ case "mj-wrapper":
2154
+ case "mj-hero": {
2155
+ let words = "";
2156
+ c.onAll((d) => {
2157
+ if (!words && d !== c && ["mj-text", "mj-button"].includes(tagOf(d))) words = textOf(d);
2158
+ });
2159
+ return words || kind;
2160
+ }
2161
+ case "mj-column": {
2162
+ const columns = c.parent()?.components().filter((s) => tagOf(s) === "mj-column") ?? [];
2163
+ return columns.length > 1 ? `${kind} ${columns.indexOf(c) + 1}` : kind;
2164
+ }
2165
+ default:
2166
+ return kind;
715
2167
  }
716
- return null;
717
2168
  }
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";
2169
+ var CONTENT = /* @__PURE__ */ new Set(["mj-text", "mj-button", "mj-navbar-link", "mj-social-element", "mj-raw"]);
2170
+ function badgeLabel(labels) {
2171
+ return (c) => {
2172
+ const tag = tagOf(c);
2173
+ const icon = ICONS[tag] ? `<div class="gjs-badge__icon">${ICONS[tag]}</div>` : "";
2174
+ const kind = labels.elementNames[tag] ?? tag.replace(/^mj-/, "");
2175
+ return `${icon}<div class="gjs-badge__name">${escapeHtml(kind)}</div>`;
2176
+ };
736
2177
  }
737
- function objectLabel(c) {
738
- const base = TYPE_LABEL[tagOf(c)] ?? tagOf(c);
739
- const text = String(c.getEl?.()?.textContent ?? "").replace(/\s+/g, " ").trim().slice(0, 30);
740
- return text ? `${base}: \u201C${text}\u201D` : base;
2178
+ function setupLayerNames(editor, labels) {
2179
+ const hideContent = (c) => c.onAll((d) => d !== c && d.set("layerable", false, { avoidStore: true }));
2180
+ for (const type of editor.Components.getTypes()) {
2181
+ if (!type.id.startsWith("mj-")) continue;
2182
+ editor.Components.addType(type.id, {
2183
+ model: {
2184
+ // Ours whatever the element says: GrapesJS writes it into the row's markup as it is.
2185
+ getIcon() {
2186
+ return ICONS[type.id] ?? "";
2187
+ },
2188
+ // GrapesJS writes the name into the row's markup unescaped, and it is document text.
2189
+ getName() {
2190
+ return escapeHtml(layerName(this, labels));
2191
+ },
2192
+ ...CONTENT.has(type.id) && {
2193
+ // Editing a text replaces its children, so they are hidden again as they arrive.
2194
+ init() {
2195
+ hideContent(this);
2196
+ this.listenTo(this.components(), "add reset", () => hideContent(this));
2197
+ }
2198
+ }
2199
+ },
2200
+ // Runs the plugin's own `init` as well.
2201
+ extendFn: ["init"]
2202
+ });
2203
+ }
2204
+ let pending = false;
2205
+ editor.on("update component:add component:remove", () => {
2206
+ if (pending) return;
2207
+ pending = true;
2208
+ requestAnimationFrame(() => {
2209
+ pending = false;
2210
+ editor.Layers.getRoot()?.onAll((c) => {
2211
+ const view = c.viewLayer;
2212
+ const el = view?.getInputName?.();
2213
+ const name = layerName(c, labels);
2214
+ if (!el || el.textContent === name) return;
2215
+ el.textContent = name;
2216
+ el.parentElement?.setAttribute("title", name);
2217
+ });
2218
+ });
2219
+ });
2220
+ }
2221
+
2222
+ // src/components/editor/inspector/StructureSection.tsx
2223
+ import { AtSign, ChevronRight, Link2 as Link22, Plus } from "lucide-react";
2224
+
2225
+ // src/components/editor/inspector/structure.ts
2226
+ function tagOf2(c) {
2227
+ return String(c.get("tagName") || c.get("type") || "");
2228
+ }
2229
+ var COLUMN_RATIOS = {
2230
+ 2: [
2231
+ [1, 1],
2232
+ [1, 2],
2233
+ [2, 1]
2234
+ ],
2235
+ 3: [
2236
+ [1, 1, 1],
2237
+ [1, 2, 1]
2238
+ ]
2239
+ };
2240
+ function columnsOf(section) {
2241
+ const children = section.components().models;
2242
+ return children.length > 0 && children.every((c) => tagOf2(c) === "mj-column") ? children : null;
2243
+ }
2244
+ function ratioWidths(ratio) {
2245
+ if (ratio.every((r) => r === ratio[0])) return null;
2246
+ const total = ratio.reduce((a, b) => a + b, 0);
2247
+ return ratio.map((r) => `${Math.round(r / total * 1e4) / 100}%`);
2248
+ }
2249
+ function currentRatio(columns) {
2250
+ const widths = columns.map((c) => String(c.getAttributes().width ?? ""));
2251
+ return COLUMN_RATIOS[columns.length]?.find((ratio) => {
2252
+ const want = ratioWidths(ratio);
2253
+ if (!want) return widths.every((w) => w === "");
2254
+ return widths.every(
2255
+ (w, i) => /%$/.test(w) && Math.abs(parseFloat(w) - parseFloat(want[i])) < 0.5
2256
+ );
2257
+ }) ?? null;
2258
+ }
2259
+ function setRatio(columns, ratio) {
2260
+ const widths = ratioWidths(ratio);
2261
+ columns.forEach((c, i) => {
2262
+ if (widths) c.addAttributes({ width: widths[i] });
2263
+ else c.removeAttributes(["width"]);
2264
+ });
2265
+ }
2266
+ var PROFILE_URLS = {
2267
+ facebook: "https://facebook.com/",
2268
+ instagram: "https://instagram.com/",
2269
+ x: "https://x.com/",
2270
+ linkedin: "https://linkedin.com/",
2271
+ youtube: "https://youtube.com/",
2272
+ pinterest: "https://pinterest.com/",
2273
+ github: "https://github.com/"
2274
+ };
2275
+ function addNetwork(social) {
2276
+ const present = new Set(
2277
+ social.components().map((c) => String(c.getAttributes().name ?? "").replace(/-noshare$/, ""))
2278
+ );
2279
+ const network = Object.keys(PROFILE_URLS).find((n) => !present.has(n)) ?? "facebook";
2280
+ return social.append(
2281
+ `<mj-social-element name="${network}-noshare" href="${PROFILE_URLS[network]}"></mj-social-element>`
2282
+ )[0];
2283
+ }
2284
+ function addNavbarLink(navbar, text) {
2285
+ return navbar.append(`<mj-navbar-link>${escapeHtml2(text)}</mj-navbar-link>`)[0];
2286
+ }
2287
+ function escapeHtml2(s) {
2288
+ return s.replace(/[&<>"']/g, (ch) => `&#${ch.charCodeAt(0)};`);
2289
+ }
2290
+
2291
+ // src/components/editor/inspector/StructureSection.tsx
2292
+ import { jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
2293
+ function tagOf3(c) {
2294
+ return String(c.get("tagName") || c.get("type") || "");
2295
+ }
2296
+ function StructureSection({
2297
+ editor,
2298
+ component,
2299
+ labels,
2300
+ locked
2301
+ }) {
2302
+ switch (tagOf3(component)) {
2303
+ case "mj-section":
2304
+ return /* @__PURE__ */ jsx6(ColumnWidths, { component, labels, locked });
2305
+ case "mj-social":
2306
+ return /* @__PURE__ */ jsx6(
2307
+ Children,
2308
+ {
2309
+ editor,
2310
+ component,
2311
+ labels,
2312
+ locked,
2313
+ title: labels.socialNetworks,
2314
+ add: labels.addNetwork,
2315
+ icon: /* @__PURE__ */ jsx6(AtSign, { className: "size-3.5 shrink-0" }),
2316
+ onAdd: () => editor.select(addNetwork(component))
2317
+ }
2318
+ );
2319
+ case "mj-navbar":
2320
+ return /* @__PURE__ */ jsx6(
2321
+ Children,
2322
+ {
2323
+ editor,
2324
+ component,
2325
+ labels,
2326
+ locked,
2327
+ title: labels.navbarLinks,
2328
+ add: labels.addLink,
2329
+ icon: /* @__PURE__ */ jsx6(Link22, { className: "size-3.5 shrink-0" }),
2330
+ onAdd: () => editor.select(addNavbarLink(component, labels.newLinkText))
2331
+ }
2332
+ );
2333
+ default:
2334
+ return null;
2335
+ }
2336
+ }
2337
+ function Heading({ children }) {
2338
+ return /* @__PURE__ */ jsx6("h3", { className: "px-3 pb-1 text-[10px] font-semibold tracking-wider text-panel-muted-fg/80 uppercase", children });
2339
+ }
2340
+ function ColumnWidths({
2341
+ component,
2342
+ labels,
2343
+ locked
2344
+ }) {
2345
+ const columns = columnsOf(component);
2346
+ const ratios = columns && COLUMN_RATIOS[columns.length];
2347
+ if (!columns || !ratios) return null;
2348
+ const current = currentRatio(columns);
2349
+ return /* @__PURE__ */ jsxs5("section", { className: "border-b border-panel-border/60 py-2", children: [
2350
+ /* @__PURE__ */ jsx6(Heading, { children: labels.columnWidths }),
2351
+ /* @__PURE__ */ jsx6("div", { className: "flex gap-1.5 px-3 py-1", children: ratios.map((ratio) => {
2352
+ const name = labels.columnRatio(ratio.join(":"));
2353
+ const active = current?.join() === ratio.join();
2354
+ return /* @__PURE__ */ jsx6(
2355
+ "button",
2356
+ {
2357
+ type: "button",
2358
+ disabled: locked,
2359
+ "aria-pressed": active,
2360
+ title: name,
2361
+ "aria-label": name,
2362
+ onClick: () => !active && setRatio(columns, ratio),
2363
+ className: cn(
2364
+ "flex h-8 flex-1 items-center gap-0.5 rounded-md border px-1.5 transition-colors",
2365
+ active ? "border-brand bg-brand/15" : "border-panel-border bg-panel-elevated hover:border-panel-muted-fg"
2366
+ ),
2367
+ children: ratio.map((part, i) => /* @__PURE__ */ jsx6(
2368
+ "span",
2369
+ {
2370
+ style: { flexGrow: part },
2371
+ className: cn(
2372
+ "h-4 basis-0 rounded-sm",
2373
+ active ? "bg-brand" : "bg-panel-muted-fg/50"
2374
+ )
2375
+ },
2376
+ i
2377
+ ))
2378
+ },
2379
+ ratio.join()
2380
+ );
2381
+ }) })
2382
+ ] });
2383
+ }
2384
+ function Children({
2385
+ editor,
2386
+ component,
2387
+ labels,
2388
+ locked,
2389
+ title,
2390
+ add,
2391
+ icon,
2392
+ onAdd
2393
+ }) {
2394
+ const children = component.components().models;
2395
+ return /* @__PURE__ */ jsxs5("section", { className: "border-b border-panel-border/60 py-2", children: [
2396
+ /* @__PURE__ */ jsx6(Heading, { children: title }),
2397
+ /* @__PURE__ */ jsx6("ul", { className: "flex flex-col px-2", children: children.map((child) => /* @__PURE__ */ jsx6("li", { children: /* @__PURE__ */ jsxs5(
2398
+ "button",
2399
+ {
2400
+ type: "button",
2401
+ onClick: () => editor.select(child),
2402
+ 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",
2403
+ children: [
2404
+ /* @__PURE__ */ jsx6("span", { className: "text-panel-muted-fg", children: icon }),
2405
+ /* @__PURE__ */ jsx6("span", { className: "min-w-0 flex-1 truncate", children: layerName(child, labels) }),
2406
+ /* @__PURE__ */ jsx6(ChevronRight, { className: "size-3 shrink-0 text-panel-muted-fg opacity-0 group-hover:opacity-100" })
2407
+ ]
2408
+ }
2409
+ ) }, child.cid)) }),
2410
+ /* @__PURE__ */ jsxs5(
2411
+ "button",
2412
+ {
2413
+ type: "button",
2414
+ disabled: locked,
2415
+ onClick: onAdd,
2416
+ 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",
2417
+ children: [
2418
+ /* @__PURE__ */ jsx6(Plus, { className: "size-3.5" }),
2419
+ add
2420
+ ]
2421
+ }
2422
+ )
2423
+ ] });
2424
+ }
2425
+
2426
+ // src/components/editor/inspector/write.ts
2427
+ function same(a, b) {
2428
+ const keys = Object.keys(a);
2429
+ return keys.length === Object.keys(b).length && keys.every((k) => k in b && a[k] === b[k]);
2430
+ }
2431
+ function createWriter(editor, component, blocked = () => false) {
2432
+ const isImage = String(component.get("tagName") || component.get("type")) === "mj-image";
2433
+ const current = () => {
2434
+ const { __p: _marker, style: _style, ...rest } = component.get("attributes");
2435
+ return rest;
2436
+ };
2437
+ let drag = null;
2438
+ const record = (start, end) => {
2439
+ component.setAttributes(start, { avoidStore: true });
2440
+ if (!same(end, start)) component.setAttributes(end);
2441
+ delete component.get("attributes").__p;
2442
+ };
2443
+ return async (attr, value, { partial, remove = [] } = {}) => {
2444
+ if (editor.getEditing() === component) {
2445
+ const view = component.getView();
2446
+ await view?.syncContent?.();
2447
+ }
2448
+ if (blocked()) return;
2449
+ if (drag && drag.attr !== attr) {
2450
+ const { start: start2 } = drag;
2451
+ drag = null;
2452
+ record(start2, current());
2453
+ }
2454
+ if (attr === "src" && isImage) return void component.set({ src: value });
2455
+ const next = current();
2456
+ for (const name of remove) delete next[name];
2457
+ if (value === "") delete next[attr];
2458
+ else next[attr] = value;
2459
+ if (partial) {
2460
+ drag ??= { attr, start: current() };
2461
+ component.setAttributes(next, { avoidStore: true });
2462
+ return;
2463
+ }
2464
+ const start = drag?.start ?? current();
2465
+ drag = null;
2466
+ record(start, next);
2467
+ };
2468
+ }
2469
+
2470
+ // src/components/editor/inspector/Inspector.tsx
2471
+ import { Fragment, jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
2472
+ var GROUPS = ["content", "layout", "spacing", "style"];
2473
+ var HIDDEN = /* @__PURE__ */ new Set(["css-class", "mj-class", "id", "style"]);
2474
+ function tagOf4(c) {
2475
+ return String(c.get("tagName") || c.get("type") || "");
2476
+ }
2477
+ function attributesOf(c) {
2478
+ const out = {};
2479
+ for (const [k, v] of Object.entries(c.getAttributes())) {
2480
+ if (v !== void 0 && v !== null && v !== false) out[k] = String(v);
2481
+ }
2482
+ if (tagOf4(c) === "mj-image") out.src = String(c.get("src") ?? "");
2483
+ return out;
2484
+ }
2485
+ function documentColors(editor) {
2486
+ const counts = /* @__PURE__ */ new Map();
2487
+ const colorValue = /(?:color|background|border)[\w-]*\s*[:=]\s*["']?[^"';>{}]*?(#[0-9a-f]{3,8}\b|rgba?\([^)]*\))/gi;
2488
+ for (const match of editor.getHtml().matchAll(colorValue)) {
2489
+ const hex = toHex(match[1], { opaque: true });
2490
+ if (hex) counts.set(hex, (counts.get(hex) ?? 0) + 1);
2491
+ }
2492
+ return [...counts.entries()].sort((a, b) => b[1] - a[1]).slice(0, 14).map(([hex]) => hex);
2493
+ }
2494
+ function Inspector({
2495
+ onEmailSettings,
2496
+ empty
2497
+ }) {
2498
+ const editor = useEditorMaybe();
2499
+ const labels = useLabels();
2500
+ const { locked } = useAgentTurn();
2501
+ const lockedNow = useRef4(locked);
2502
+ useLayoutEffect(() => {
2503
+ lockedNow.current = locked;
2504
+ });
2505
+ const [, refresh] = useReducer((n) => n + 1, 0);
2506
+ const root = useRef4(null);
2507
+ const refocus = useRef4(false);
2508
+ useEffect4(() => {
2509
+ if (!editor) return;
2510
+ const events = "component:update change:device";
2511
+ const toggled = () => {
2512
+ if (root.current?.contains(document.activeElement)) refocus.current = true;
2513
+ refresh();
2514
+ };
2515
+ editor.on(events, refresh);
2516
+ editor.on("component:toggled", toggled);
2517
+ return () => {
2518
+ editor.off(events, refresh);
2519
+ editor.off("component:toggled", toggled);
2520
+ };
2521
+ }, [editor]);
2522
+ useEffect4(() => {
2523
+ if (!refocus.current) return;
2524
+ refocus.current = false;
2525
+ root.current?.querySelector("[data-focus-target]")?.focus();
2526
+ });
2527
+ if (!editor) return null;
2528
+ const selected = editor.getSelectedAll();
2529
+ const component = selected[0];
2530
+ 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(
2531
+ "p",
2532
+ {
2533
+ "data-focus-target": true,
2534
+ tabIndex: -1,
2535
+ className: "px-4 py-8 text-center text-xs text-panel-muted-fg outline-none",
2536
+ children: labels.multipleSelected(selected.length)
2537
+ }
2538
+ ) : /* @__PURE__ */ jsx7(
2539
+ ElementSettings,
2540
+ {
2541
+ editor,
2542
+ component,
2543
+ labels,
2544
+ locked,
2545
+ lockedNow,
2546
+ onEmailSettings
2547
+ },
2548
+ component.cid
2549
+ ) });
2550
+ }
2551
+ function ElementSettings({
2552
+ editor,
2553
+ component,
2554
+ labels,
2555
+ locked,
2556
+ lockedNow,
2557
+ onEmailSettings
2558
+ }) {
2559
+ const [advancedOpen, setAdvancedOpen] = useState4(false);
2560
+ const tag = tagOf4(component);
2561
+ const fields = SCHEMA[tag] ?? [];
2562
+ const attributes = attributesOf(component);
2563
+ const visible = fields.filter((f) => !f.when || f.when(attributes));
2564
+ const t = (key) => labels.inspectorLabels[key] ?? key;
2565
+ const optionName = (o) => labels.inspectorLabels[o.label] ?? o.label;
2566
+ const [writer] = useState4(() => createWriter(editor, component, () => lockedNow.current));
2567
+ const write = (attr, value, options) => lockedNow.current ? Promise.resolve() : writer(attr, value, options);
2568
+ const writeBox = (attr, shorthand, partial) => write(attr, shorthand, { remove: SIDES.map((s) => `${attr}-${s}`), partial });
2569
+ const chooseImage = (attr) => {
2570
+ if (attr === "src" && tag === "mj-image") return void editor.runCommand("open-image-picker");
2571
+ const am = editor.AssetManager;
2572
+ am.open({
2573
+ types: ["image"],
2574
+ current: imageUrl(attributes[attr] ?? "") ?? void 0,
2575
+ select(asset, complete) {
2576
+ void write(attr, typeof asset === "string" ? asset : asset.getSrc());
2577
+ if (complete) am.close();
2578
+ }
2579
+ });
2580
+ };
2581
+ const safeLink = (attr) => (link) => {
2582
+ if (tag === "mj-navbar-link" && attr === "href") {
2583
+ return isSafeNavbarLink(String(component.parent()?.getAttributes()["base-url"] ?? ""), link);
2584
+ }
2585
+ if (tag === "mj-navbar" && attr === "base-url") {
2586
+ return component.components().every((c) => isSafeNavbarLink(link, String(c.getAttributes().href ?? "")));
2587
+ }
2588
+ return isSafeLink(link);
2589
+ };
2590
+ const control = (f) => {
2591
+ const value = attributes[f.attr] ?? "";
2592
+ const c = f.control;
2593
+ switch (c.type) {
2594
+ case "text":
2595
+ return /* @__PURE__ */ jsx7(
2596
+ TextInput,
2597
+ {
2598
+ value,
2599
+ placeholder: c.placeholder ?? f.default,
2600
+ onCommit: (v) => write(f.attr, v)
2601
+ }
2602
+ );
2603
+ case "link":
2604
+ return /* @__PURE__ */ jsx7(
2605
+ TextInput,
2606
+ {
2607
+ value,
2608
+ placeholder: "https://",
2609
+ accept: safeLink(f.attr),
2610
+ onCommit: (v) => write(f.attr, v)
2611
+ }
2612
+ );
2613
+ case "color":
2614
+ return /* @__PURE__ */ jsx7(
2615
+ ColorField,
2616
+ {
2617
+ value,
2618
+ placeholder: f.default,
2619
+ documentColors: () => documentColors(editor),
2620
+ labels,
2621
+ onChange: (v, partial) => write(f.attr, v, { partial })
2622
+ }
2623
+ );
2624
+ case "image":
2625
+ return /* @__PURE__ */ jsx7(
2626
+ ImageField,
2627
+ {
2628
+ value,
2629
+ labels,
2630
+ onChoose: () => chooseImage(f.attr),
2631
+ onClear: () => write(f.attr, "")
2632
+ }
2633
+ );
2634
+ case "length":
2635
+ return /* @__PURE__ */ jsx7(
2636
+ LengthInput,
2637
+ {
2638
+ value,
2639
+ placeholder: f.default,
2640
+ units: c.units,
2641
+ negative: c.negative,
2642
+ labels,
2643
+ onCommit: (v, partial) => write(f.attr, v, { partial })
2644
+ }
2645
+ );
2646
+ case "box":
2647
+ return /* @__PURE__ */ jsx7(
2648
+ BoxInput,
2649
+ {
2650
+ attributes,
2651
+ attr: f.attr,
2652
+ placeholder: f.default,
2653
+ units: c.units,
2654
+ labels,
2655
+ onCommit: (v, partial) => writeBox(f.attr, v, partial)
2656
+ }
2657
+ );
2658
+ case "segmented":
2659
+ return /* @__PURE__ */ jsx7(
2660
+ Segmented,
2661
+ {
2662
+ value,
2663
+ fallback: f.default,
2664
+ options: c.options,
2665
+ name: optionName,
2666
+ onCommit: (v) => write(f.attr, v)
2667
+ }
2668
+ );
2669
+ case "select":
2670
+ return /* @__PURE__ */ jsx7(
2671
+ Select,
2672
+ {
2673
+ value,
2674
+ fallback: f.default,
2675
+ options: c.options,
2676
+ name: optionName,
2677
+ onCommit: (v) => write(f.attr, v)
2678
+ }
2679
+ );
2680
+ case "toggle":
2681
+ return /* @__PURE__ */ jsx7(
2682
+ Toggle,
2683
+ {
2684
+ checked: value === c.on,
2685
+ label: t(f.label),
2686
+ onCommit: (on) => write(f.attr, on ? c.on : "")
2687
+ }
2688
+ );
2689
+ case "network": {
2690
+ const { network, share } = parseNetwork(value);
2691
+ return /* @__PURE__ */ jsxs6("div", { className: "flex flex-col gap-1.5", children: [
2692
+ /* @__PURE__ */ jsx7(
2693
+ Select,
2694
+ {
2695
+ value: network,
2696
+ options: c.networks,
2697
+ name: optionName,
2698
+ onCommit: (n) => write(f.attr, n ? networkName(n, share) : "")
2699
+ }
2700
+ ),
2701
+ network && network !== "custom" && /* @__PURE__ */ jsx7(
2702
+ Segmented,
2703
+ {
2704
+ value: share ? "share" : "profile",
2705
+ options: [
2706
+ { value: "profile", label: labels.linkTypeProfile },
2707
+ { value: "share", label: labels.linkTypeShare }
2708
+ ],
2709
+ name: (o) => o.label,
2710
+ onCommit: (v) => write(f.attr, networkName(network, v === "share"))
2711
+ }
2712
+ )
2713
+ ] });
2714
+ }
2715
+ }
2716
+ };
2717
+ const own = (f) => f.control.type === "box" ? [f.attr, ...SIDES.map((s) => `${f.attr}-${s}`)].some((a) => attributes[a] !== void 0) : !!attributes[f.attr];
2718
+ const reset = (f) => f.control.type === "box" ? writeBox(f.attr, "") : write(f.attr, "");
2719
+ const wide = (f) => ["color", "image", "link", "text", "box", "network"].includes(f.control.type);
2720
+ const row = (f) => /* @__PURE__ */ jsx7(
2721
+ Row,
2722
+ {
2723
+ label: t(f.label),
2724
+ help: labels.inspectorLabels[`${f.label}.help`],
2725
+ own: own(f),
2726
+ wide: wide(f),
2727
+ inline: f.control.type === "toggle",
2728
+ labels,
2729
+ onReset: () => void reset(f),
2730
+ children: control(f)
2731
+ },
2732
+ f.attr
2733
+ );
2734
+ const named = new Set(
2735
+ fields.flatMap(
2736
+ (f) => f.control.type === "box" ? [f.attr, ...SIDES.map((s) => `${f.attr}-${s}`)] : [f.attr]
2737
+ )
2738
+ );
2739
+ const others = Object.entries(attributes).filter(([k]) => !named.has(k) && !HIDDEN.has(k));
2740
+ const advanced = visible.filter((f) => f.group === "advanced");
2741
+ const advancedCount = advanced.length + others.length;
2742
+ return /* @__PURE__ */ jsxs6("div", { className: "pb-6", children: [
2743
+ /* @__PURE__ */ jsx7(
2744
+ Header,
2745
+ {
2746
+ editor,
2747
+ component,
2748
+ labels,
2749
+ onEmailSettings
2750
+ }
2751
+ ),
2752
+ 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: [
2753
+ /* @__PURE__ */ jsx7(Monitor, { className: "size-3.5 shrink-0" }),
2754
+ labels.allScreenSizes
2755
+ ] }),
2756
+ /* @__PURE__ */ jsx7(StructureSection, { editor, component, labels, locked }),
2757
+ GROUPS.map((group) => {
2758
+ const inGroup = visible.filter((f) => f.group === group);
2759
+ if (inGroup.length === 0) return null;
2760
+ return /* @__PURE__ */ jsxs6("section", { className: "border-b border-panel-border/60 py-2", children: [
2761
+ /* @__PURE__ */ jsx7("h3", { className: "px-3 pb-1 text-[10px] font-semibold tracking-wider text-panel-muted-fg/80 uppercase", children: groupName(group, labels) }),
2762
+ inGroup.map(row)
2763
+ ] }, group);
2764
+ }),
2765
+ advancedCount > 0 && /* @__PURE__ */ jsxs6("section", { className: "py-1", children: [
2766
+ /* @__PURE__ */ jsxs6(
2767
+ "button",
2768
+ {
2769
+ type: "button",
2770
+ onClick: () => setAdvancedOpen((o) => !o),
2771
+ "aria-expanded": advancedOpen,
2772
+ 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",
2773
+ children: [
2774
+ /* @__PURE__ */ jsx7(
2775
+ ChevronRight2,
2776
+ {
2777
+ className: cn("size-3 transition-transform", advancedOpen && "rotate-90")
2778
+ }
2779
+ ),
2780
+ labels.groupAdvanced,
2781
+ /* @__PURE__ */ jsxs6("span", { className: "font-normal normal-case", children: [
2782
+ "(",
2783
+ advancedCount,
2784
+ ")"
2785
+ ] })
2786
+ ]
2787
+ }
2788
+ ),
2789
+ advancedOpen && /* @__PURE__ */ jsxs6(Fragment, { children: [
2790
+ advanced.map(row),
2791
+ others.length > 0 && /* @__PURE__ */ jsxs6("div", { className: "px-3 pt-2", children: [
2792
+ /* @__PURE__ */ jsx7("p", { className: "pb-1 text-[11px] text-panel-muted-fg", children: labels.otherAttributes }),
2793
+ /* @__PURE__ */ jsx7("ul", { className: "flex flex-col gap-1", children: others.map(([k, v]) => /* @__PURE__ */ jsxs6(
2794
+ "li",
2795
+ {
2796
+ className: "flex items-center gap-2 rounded-md bg-panel-elevated px-2 py-1 font-mono text-[11px]",
2797
+ children: [
2798
+ /* @__PURE__ */ jsx7("span", { className: "text-panel-muted-fg", children: k }),
2799
+ /* @__PURE__ */ jsx7("span", { className: "min-w-0 flex-1 truncate text-panel-fg", title: v, children: v }),
2800
+ /* @__PURE__ */ jsx7(
2801
+ "button",
2802
+ {
2803
+ type: "button",
2804
+ onClick: () => void write(k, ""),
2805
+ title: labels.removeAttribute,
2806
+ "aria-label": `${labels.removeAttribute}: ${k}`,
2807
+ className: "text-panel-muted-fg hover:text-panel-fg",
2808
+ children: /* @__PURE__ */ jsx7(X2, { className: "size-3" })
2809
+ }
2810
+ )
2811
+ ]
2812
+ },
2813
+ k
2814
+ )) })
2815
+ ] })
2816
+ ] })
2817
+ ] })
2818
+ ] });
2819
+ }
2820
+ function groupName(group, labels) {
2821
+ return {
2822
+ content: labels.groupContent,
2823
+ layout: labels.groupLayout,
2824
+ spacing: labels.groupSpacing,
2825
+ style: labels.groupStyle
2826
+ }[group];
2827
+ }
2828
+ function Header({
2829
+ editor,
2830
+ component,
2831
+ labels,
2832
+ onEmailSettings
2833
+ }) {
2834
+ const tag = tagOf4(component);
2835
+ const isBody = tag === "mj-body";
2836
+ const name = isBody ? labels.emailSettings : labels.elementNames[tag] ?? tag.replace(/^mj-/, "");
2837
+ const words = tag === "mj-column" ? "" : layerName(component, labels);
2838
+ const excerpt = words === name ? "" : words;
2839
+ const parent = component.parent();
2840
+ const action = "flex size-7 items-center justify-center rounded-md text-panel-muted-fg hover:bg-panel-hover hover:text-panel-fg";
2841
+ 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: [
2842
+ /* @__PURE__ */ jsxs6("div", { className: "min-w-0 flex-1", children: [
2843
+ /* @__PURE__ */ jsx7(
2844
+ "p",
2845
+ {
2846
+ "data-focus-target": true,
2847
+ tabIndex: -1,
2848
+ className: "truncate text-sm font-medium text-panel-fg outline-none",
2849
+ children: name
2850
+ }
2851
+ ),
2852
+ excerpt && !isBody && /* @__PURE__ */ jsxs6("p", { className: "truncate text-[11px] text-panel-muted-fg", children: [
2853
+ "\u201C",
2854
+ excerpt,
2855
+ "\u201D"
2856
+ ] })
2857
+ ] }),
2858
+ !isBody && parent && /* @__PURE__ */ jsx7(
2859
+ "button",
2860
+ {
2861
+ type: "button",
2862
+ className: action,
2863
+ title: labels.selectParent,
2864
+ "aria-label": labels.selectParent,
2865
+ onClick: () => tagOf4(parent) === "mj-body" ? onEmailSettings() : editor.select(parent),
2866
+ children: /* @__PURE__ */ jsx7(ArrowUp, { className: "size-3.5" })
2867
+ }
2868
+ ),
2869
+ !isBody && /* @__PURE__ */ jsxs6(Fragment, { children: [
2870
+ /* @__PURE__ */ jsx7(
2871
+ "button",
2872
+ {
2873
+ type: "button",
2874
+ className: action,
2875
+ title: labels.duplicate,
2876
+ "aria-label": labels.duplicate,
2877
+ onClick: () => editor.runCommand("tlb-clone"),
2878
+ children: /* @__PURE__ */ jsx7(Copy, { className: "size-3.5" })
2879
+ }
2880
+ ),
2881
+ /* @__PURE__ */ jsx7(
2882
+ "button",
2883
+ {
2884
+ type: "button",
2885
+ className: action,
2886
+ title: labels.deleteElement,
2887
+ "aria-label": labels.deleteElement,
2888
+ onClick: () => editor.runCommand("tlb-delete"),
2889
+ children: /* @__PURE__ */ jsx7(Trash22, { className: "size-3.5" })
2890
+ }
2891
+ )
2892
+ ] })
2893
+ ] });
2894
+ }
2895
+
2896
+ // src/components/editor/ids.ts
2897
+ var ID_RE = { sec: /\bsec-([A-Za-z0-9_-]+)\b/, obj: /\bobj-([A-Za-z0-9_-]+)\b/ };
2898
+ function idOf(c, prefix) {
2899
+ return String(c.getAttributes()["css-class"] ?? "").match(ID_RE[prefix])?.[1] ?? null;
2900
+ }
2901
+ var newId = () => Math.random().toString(36).slice(2, 10);
2902
+ function sectionIdOf(c) {
2903
+ return idOf(c, "sec");
2904
+ }
2905
+ function addIdClass(c, prefix) {
2906
+ const existing = idOf(c, prefix);
2907
+ if (existing) return existing;
2908
+ const cssClass = String(c.getAttributes()["css-class"] ?? "");
2909
+ const id = newId();
2910
+ c.addAttributes({ "css-class": `${cssClass} ${prefix}-${id}`.trim() });
2911
+ return id;
2912
+ }
2913
+ function freshenCopiedIds(root, added) {
2914
+ const used = (prefix, id, except) => {
2915
+ let found = false;
2916
+ root?.onAll((o) => {
2917
+ if (o !== except && idOf(o, prefix) === id) found = true;
2918
+ });
2919
+ return found;
2920
+ };
2921
+ added.onAll((c) => {
2922
+ for (const prefix of ["sec", "obj"]) {
2923
+ const id = idOf(c, prefix);
2924
+ if (!id || !used(prefix, id, c)) continue;
2925
+ const cssClass = String(c.getAttributes()["css-class"]);
2926
+ c.addAttributes({ "css-class": cssClass.replace(`${prefix}-${id}`, `${prefix}-${newId()}`) });
2927
+ }
2928
+ });
2929
+ }
2930
+
2931
+ // src/components/editor/plugin-types.ts
2932
+ function fixPluginTypes(editor) {
2933
+ for (const type of editor.Components.getTypes()) {
2934
+ if (!type.id.startsWith("mj-")) continue;
2935
+ const base = type.model.prototype.getAttrToHTML;
2936
+ editor.Components.addType(type.id, {
2937
+ model: {
2938
+ defaults: { "style-default": {} },
2939
+ getAttrToHTML() {
2940
+ const attributes = base.call(this);
2941
+ delete attributes.__p;
2942
+ for (const [name, value] of Object.entries(attributes)) {
2943
+ if (typeof value === "string") attributes[name] = value.replace(/"/g, "&quot;");
2944
+ }
2945
+ return attributes;
2946
+ }
2947
+ }
2948
+ });
2949
+ }
2950
+ }
2951
+
2952
+ // src/components/editor/EmailEditor.tsx
2953
+ import { jsx as jsx8, jsxs as jsxs7 } from "react/jsx-runtime";
2954
+ var TYPE_LABEL = {
2955
+ "mj-text": "Text",
2956
+ "mj-button": "Button",
2957
+ "mj-image": "Image",
2958
+ "mj-column": "Column"
2959
+ };
2960
+ function tagOf5(c) {
2961
+ return String(c.get("tagName") || c.get("type") || "");
2962
+ }
2963
+ 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>';
2964
+ 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>';
2965
+ function isSection(c) {
2966
+ return tagOf5(c) === "mj-section";
2967
+ }
2968
+ function isCommentable(c) {
2969
+ const t = tagOf5(c);
2970
+ return t === "mj-section" || t in TYPE_LABEL;
2971
+ }
2972
+ function closestSection(c) {
2973
+ let cur = c;
2974
+ while (cur) {
2975
+ if (isSection(cur)) return cur;
2976
+ cur = cur.parent() ?? void 0;
2977
+ }
2978
+ return null;
2979
+ }
2980
+ function crumbName(c, labels) {
2981
+ const t = tagOf5(c);
2982
+ return labels.elementNames[t] ?? t.replace(/^mj-/, "");
2983
+ }
2984
+ var BLOCKS = [
2985
+ "mj-1-column",
2986
+ "mj-2-columns",
2987
+ "mj-3-columns",
2988
+ "mj-text",
2989
+ "mj-image",
2990
+ "mj-button",
2991
+ "mj-divider",
2992
+ "mj-spacer",
2993
+ "mj-social-group",
2994
+ "mj-navbar"
2995
+ ];
2996
+ var LAYOUT_BLOCKS = /* @__PURE__ */ new Set(["mj-1-column", "mj-2-columns", "mj-3-columns"]);
2997
+ var SOCIAL_BLOCK = `<mj-social icon-size="24px" mode="horizontal">
2998
+ <mj-social-element name="facebook-noshare" href="https://facebook.com/"></mj-social-element>
2999
+ <mj-social-element name="instagram-noshare" href="https://instagram.com/"></mj-social-element>
3000
+ <mj-social-element name="x-noshare" href="https://x.com/"></mj-social-element>
3001
+ </mj-social>`;
3002
+ function objectLabel(c) {
3003
+ const base = TYPE_LABEL[tagOf5(c)] ?? tagOf5(c);
3004
+ const text = String(c.getEl?.()?.textContent ?? "").replace(/\s+/g, " ").trim().slice(0, 30);
3005
+ return text ? `${base}: \u201C${text}\u201D` : base;
741
3006
  }
742
3007
  var RTE_FONTS = [
743
3008
  "Arial",
@@ -767,75 +3032,6 @@ function wrapSelectionStyle(el, style) {
767
3032
  } catch {
768
3033
  }
769
3034
  }
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
3035
  function setupRichText(editor, labels) {
840
3036
  const rte = editor.RichTextEditor;
841
3037
  if (!rte || rte.__customActions) return;
@@ -875,23 +3071,29 @@ function setupRichText(editor, labels) {
875
3071
  function EmailEditor({ docId, onReady, commentsRefresh, onOpenCountChange }) {
876
3072
  const documents = useDocumentStore();
877
3073
  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);
3074
+ const loadingRef = useRef5(false);
3075
+ const saveStatusRef = useRef5("idle");
3076
+ const stateListeners = useRef5(/* @__PURE__ */ new Set());
3077
+ const [loading, setLoading] = useState5(true);
3078
+ const [sidebarView, setSidebarView] = useState5("blocks");
3079
+ const blockSearchRef = useRef5(null);
3080
+ const onBlockSearchKey = (e) => {
3081
+ if (readOnlyRef.current || !opensBlockSearch(e)) return;
3082
+ e.preventDefault();
3083
+ setSidebarView("blocks");
3084
+ requestAnimationFrame(() => blockSearchRef.current?.focus());
3085
+ };
3086
+ const [composeTarget, setComposeTarget] = useState5(null);
3087
+ const selectionSource = useRef5("other");
3088
+ const editorRef = useRef5(null);
886
3089
  const { running: turnRunning, locked: readOnly, setLocked } = useAgentTurn();
887
- const readOnlyRef = useRef3(false);
888
- const applyReadOnly = useRef3(async () => {
3090
+ const readOnlyRef = useRef5(false);
3091
+ const applyReadOnly = useRef5(async () => {
889
3092
  });
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(
3093
+ useEffect5(() => void applyReadOnly.current(turnRunning), [turnRunning]);
3094
+ useEffect5(() => () => setLocked(false), [setLocked]);
3095
+ const [crumbs, setCrumbs] = useState5([]);
3096
+ const [picker, setPicker] = useState5(
895
3097
  null
896
3098
  );
897
3099
  const onEditor = async (editor) => {
@@ -900,7 +3102,7 @@ function EmailEditor({ docId, onReady, commentsRefresh, onOpenCountChange }) {
900
3102
  const chain = [];
901
3103
  let cur = c;
902
3104
  while (cur) {
903
- const t = tagOf(cur);
3105
+ const t = tagOf5(cur);
904
3106
  if (!t || t === "mj-body" || t === "wrapper" || t === "Wrapper") break;
905
3107
  chain.unshift(cur);
906
3108
  cur = cur.parent() ?? void 0;
@@ -949,7 +3151,7 @@ function EmailEditor({ docId, onReady, commentsRefresh, onOpenCountChange }) {
949
3151
  if (isSection(c)) addIdClass(c, "sec");
950
3152
  if (!isCommentable(c)) return;
951
3153
  const toolbar = [...c.get("toolbar") ?? []];
952
- if (tagOf(c) === "mj-image" && !toolbar.some((t) => t.command === "open-image-picker")) {
3154
+ if (tagOf5(c) === "mj-image" && !toolbar.some((t) => t.command === "open-image-picker")) {
953
3155
  toolbar.push({
954
3156
  command: "open-image-picker",
955
3157
  label: IMAGE_SVG,
@@ -957,11 +3159,11 @@ function EmailEditor({ docId, onReady, commentsRefresh, onOpenCountChange }) {
957
3159
  });
958
3160
  c.set("toolbar", toolbar);
959
3161
  }
960
- if (!toolbar.some((t) => t.command === "open-comments")) {
3162
+ if (closestSection(c) && !toolbar.some((t) => t.command === "open-comments")) {
961
3163
  toolbar.push({
962
3164
  command: "open-comments",
963
3165
  label: SPARKLES_SVG,
964
- attributes: { title: "Comment on element / section", "data-role": "sparkles" }
3166
+ attributes: { title: labels.commentOnElement, "data-role": "sparkles" }
965
3167
  });
966
3168
  c.set("toolbar", toolbar);
967
3169
  }
@@ -969,7 +3171,7 @@ function EmailEditor({ docId, onReady, commentsRefresh, onOpenCountChange }) {
969
3171
  editor.Commands.add("open-image-picker", {
970
3172
  run(ed) {
971
3173
  const target = ed.getSelected();
972
- if (!target || tagOf(target) !== "mj-image") return;
3174
+ if (!target || tagOf5(target) !== "mj-image") return;
973
3175
  const am = ed.AssetManager;
974
3176
  am.open({
975
3177
  target,
@@ -988,21 +3190,28 @@ function EmailEditor({ docId, onReady, commentsRefresh, onOpenCountChange }) {
988
3190
  }
989
3191
  });
990
3192
  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";
3193
+ fixPluginTypes(editor);
3194
+ setupLayerNames(editor, labels);
3195
+ const STRUCTURE = /* @__PURE__ */ new Set(["mj-wrapper", "mj-section", "mj-column"]);
3196
+ const showBodyInLayers = () => {
3197
+ const body = findByTag("mj-body");
3198
+ if (!body) return;
3199
+ editor.Layers.setRoot(body);
3200
+ body.onAll((c) => {
3201
+ if (STRUCTURE.has(tagOf5(c))) editor.Layers.setOpen(c, true);
3202
+ });
997
3203
  };
998
- const bm = editor.BlockManager;
999
- bm.getAll().forEach(
1000
- (b) => {
1001
- b.set("category", blockCategory(b.get("label") || ""));
3204
+ const keepStructureOpen = () => {
3205
+ const layers = editor.Layers;
3206
+ const opened = layers.model?.get("opened") ?? {};
3207
+ for (const [cid, c] of Object.entries(opened)) {
3208
+ if (STRUCTURE.has(tagOf5(c))) delete opened[cid];
1002
3209
  }
1003
- );
1004
- bm.render();
1005
- editor.on("component:add", decorate);
3210
+ };
3211
+ editor.on("component:add", (c) => {
3212
+ if (!loadingRef.current) freshenCopiedIds(editor.getWrapper(), c);
3213
+ decorate(c);
3214
+ });
1006
3215
  editor.on("load", () => {
1007
3216
  const body = editor.Canvas.getBody();
1008
3217
  body?.addEventListener(
@@ -1012,34 +3221,17 @@ function EmailEditor({ docId, onReady, commentsRefresh, onOpenCountChange }) {
1012
3221
  },
1013
3222
  true
1014
3223
  );
3224
+ editor.Canvas.getDocument()?.addEventListener("keydown", onBlockSearchKey);
3225
+ showBodyInLayers();
1015
3226
  });
1016
3227
  editor.on("component:selected", (c) => {
3228
+ keepStructureOpen();
1017
3229
  if (selectionSource.current === "canvas") setSidebarView("settings");
1018
3230
  selectionSource.current = "other";
1019
3231
  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
3232
  });
1034
3233
  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") ?? ""));
3234
+ buildCrumbs(editor.getSelected());
1043
3235
  });
1044
3236
  let stored = "";
1045
3237
  const save = async () => {
@@ -1113,17 +3305,30 @@ function EmailEditor({ docId, onReady, commentsRefresh, onOpenCountChange }) {
1113
3305
  });
1114
3306
  const loadMjml = (mjml) => {
1115
3307
  loadingRef.current = true;
3308
+ let repaired = false;
1116
3309
  try {
1117
3310
  queue.cancel();
1118
3311
  stored = mjml;
1119
- editor.setComponents(mjml || STARTER_MJML);
3312
+ const source = withoutEditorProps(mjml || STARTER_MJML);
3313
+ repaired = source !== (mjml || STARTER_MJML);
3314
+ editor.setComponents(source);
1120
3315
  editor.getWrapper()?.find("mj-section").forEach(decorate);
1121
3316
  for (const t of Object.keys(TYPE_LABEL)) {
1122
3317
  editor.getWrapper()?.find(t).forEach(decorate);
1123
3318
  }
3319
+ editor.getWrapper()?.onAll((c) => {
3320
+ const illegal = illegalAttributes(tagOf5(c), c.getAttributes());
3321
+ if (illegal.length === 0) return;
3322
+ c.removeAttributes(illegal);
3323
+ repaired = true;
3324
+ });
3325
+ showBodyInLayers();
3326
+ editor.UndoManager.clear();
1124
3327
  } finally {
1125
3328
  loadingRef.current = false;
1126
3329
  }
3330
+ notifyState();
3331
+ if (repaired && !readOnlyRef.current) queue.schedule();
1127
3332
  };
1128
3333
  const ensureHighlightStyles = () => {
1129
3334
  const cdoc = editor.Canvas.getDocument();
@@ -1210,7 +3415,7 @@ function EmailEditor({ docId, onReady, commentsRefresh, onOpenCountChange }) {
1210
3415
  }
1211
3416
  });
1212
3417
  };
1213
- return /* @__PURE__ */ jsx5(
3418
+ return /* @__PURE__ */ jsx8(
1214
3419
  GjsEditor,
1215
3420
  {
1216
3421
  grapesjs,
@@ -1218,12 +3423,16 @@ function EmailEditor({ docId, onReady, commentsRefresh, onOpenCountChange }) {
1218
3423
  options: {
1219
3424
  height: "100%",
1220
3425
  storageManager: false,
3426
+ // On by default: every visitor's browser would post the host's domain to
3427
+ // app.grapesjs.com and leave a session-storage entry, which a host would then have
3428
+ // to disclose and, for the entry, ask consent for.
3429
+ telemetry: false,
1221
3430
  fromElement: false,
1222
3431
  panels: { defaults: [] },
1223
3432
  blockManager: { appendTo: "#gjs-blocks" },
1224
- styleManager: { appendTo: "#gjs-styles" },
1225
- traitManager: { appendTo: "#gjs-traits" },
1226
- layerManager: { appendTo: "#gjs-layers" },
3433
+ // The eye hides a layer by writing `display="none"` into the MJML, which is illegal.
3434
+ // The root row is mj-body, which the sidebar reaches as "Email settings" instead.
3435
+ layerManager: { appendTo: "#gjs-layers", hidable: false, showWrapper: false },
1227
3436
  assetManager: {
1228
3437
  // Never inline files: mail clients block `data:` images (see ImageLibrary).
1229
3438
  embedAsBase64: false,
@@ -1237,110 +3446,106 @@ function EmailEditor({ docId, onReady, commentsRefresh, onOpenCountChange }) {
1237
3446
  close: () => setPicker(null)
1238
3447
  }
1239
3448
  },
3449
+ canvas: { customBadgeLabel: badgeLabel(labels) },
1240
3450
  deviceManager: {
1241
3451
  devices: [
1242
3452
  { id: "desktop", name: "Desktop", width: "" },
1243
3453
  { id: "mobile", name: "Mobile", width: "375px", widthMedia: "480px" }
1244
3454
  ]
1245
3455
  },
1246
- plugins: [grapesjsMJML]
3456
+ plugins: [
3457
+ (editor) => grapesjsMJML(editor, {
3458
+ blocks: BLOCKS,
3459
+ block: (id) => ({
3460
+ label: labels.blockLabels[id] ?? id,
3461
+ // By id: a regex on the label broke as soon as a host translated it.
3462
+ category: LAYOUT_BLOCKS.has(id) ? labels.blockCategoryLayout : labels.blockCategoryContent,
3463
+ ...id === "mj-social-group" && { content: SOCIAL_BLOCK }
3464
+ })
3465
+ })
3466
+ ]
1247
3467
  },
1248
3468
  onEditor,
1249
- children: /* @__PURE__ */ jsxs4("div", { className: "relative flex h-full min-h-0 flex-1", inert: readOnly, children: [
1250
- 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: [
1251
- /* @__PURE__ */ jsx5(Spinner, {}),
1252
- " ",
1253
- labels.agentWorking
1254
- ] }) }),
1255
- /* @__PURE__ */ jsx5(
1256
- LeftSidebar,
1257
- {
1258
- view: sidebarView,
1259
- onViewChange: setSidebarView,
1260
- markLayersSource: () => selectionSource.current = "layers",
1261
- selectedImageSrc,
1262
- onChooseImage: () => editorRef.current?.runCommand("open-image-picker")
1263
- }
1264
- ),
1265
- /* @__PURE__ */ jsxs4("div", { className: "flex min-w-0 flex-1 flex-col bg-surface-muted", children: [
1266
- /* @__PURE__ */ jsxs4("div", { className: "relative min-h-0 flex-1", children: [
1267
- /* @__PURE__ */ jsx5(Canvas, { className: "h-full" }),
1268
- /* @__PURE__ */ jsx5(
1269
- CanvasComments,
1270
- {
1271
- docId,
1272
- composeTarget,
1273
- onComposeConsumed: () => setComposeTarget(null),
1274
- refreshSignal: commentsRefresh,
1275
- onOpenCountChange
1276
- }
1277
- ),
1278
- /* @__PURE__ */ jsx5(
1279
- ImagePicker,
3469
+ children: /* @__PURE__ */ jsxs7(
3470
+ "div",
3471
+ {
3472
+ className: "relative flex h-full min-h-0 flex-1",
3473
+ inert: readOnly,
3474
+ onKeyDown: (e) => onBlockSearchKey(e.nativeEvent),
3475
+ children: [
3476
+ 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: [
3477
+ /* @__PURE__ */ jsx8(Spinner, {}),
3478
+ " ",
3479
+ labels.agentWorking
3480
+ ] }) }),
3481
+ /* @__PURE__ */ jsx8(
3482
+ LeftSidebar,
1280
3483
  {
1281
- open: picker !== null,
1282
- currentUrl: picker?.current,
1283
- onSelect: (url) => {
1284
- if (!readOnlyRef.current) picker?.select(url);
1285
- setPicker(null);
1286
- },
1287
- onClose: () => {
1288
- setPicker(null);
1289
- editorRef.current?.AssetManager.close();
1290
- }
3484
+ view: sidebarView,
3485
+ onViewChange: setSidebarView,
3486
+ markLayersSource: () => selectionSource.current = "layers",
3487
+ blockSearchRef
1291
3488
  }
1292
3489
  ),
1293
- 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: [
1294
- /* @__PURE__ */ jsx5(Spinner, {}),
1295
- " Loading editor\u2026"
3490
+ /* @__PURE__ */ jsxs7("div", { className: "flex min-w-0 flex-1 flex-col bg-surface-muted", children: [
3491
+ /* @__PURE__ */ jsxs7("div", { className: "relative min-h-0 flex-1", children: [
3492
+ /* @__PURE__ */ jsx8(Canvas, { className: "h-full" }),
3493
+ /* @__PURE__ */ jsx8(
3494
+ CanvasComments,
3495
+ {
3496
+ docId,
3497
+ composeTarget,
3498
+ onComposeConsumed: () => setComposeTarget(null),
3499
+ refreshSignal: commentsRefresh,
3500
+ onOpenCountChange
3501
+ }
3502
+ ),
3503
+ /* @__PURE__ */ jsx8(
3504
+ ImagePicker,
3505
+ {
3506
+ open: picker !== null,
3507
+ currentUrl: picker?.current,
3508
+ onSelect: (url) => {
3509
+ if (!readOnlyRef.current) picker?.select(url);
3510
+ setPicker(null);
3511
+ },
3512
+ onClose: () => {
3513
+ setPicker(null);
3514
+ editorRef.current?.AssetManager.close();
3515
+ }
3516
+ }
3517
+ ),
3518
+ 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: [
3519
+ /* @__PURE__ */ jsx8(Spinner, {}),
3520
+ " ",
3521
+ labels.loadingEditor
3522
+ ] })
3523
+ ] }),
3524
+ /* @__PURE__ */ jsx8(Breadcrumb, { crumbs, onSelect: (c) => editorRef.current?.select(c) })
1296
3525
  ] })
1297
- ] }),
1298
- /* @__PURE__ */ jsx5(Breadcrumb, { crumbs, onSelect: (c) => editorRef.current?.select(c) })
1299
- ] })
1300
- ] })
3526
+ ]
3527
+ }
3528
+ )
1301
3529
  }
1302
3530
  );
1303
3531
  }
1304
- function CollapseSection({
1305
- title,
1306
- defaultOpen = true,
1307
- children
1308
- }) {
1309
- const [open, setOpen] = useState3(defaultOpen);
1310
- return /* @__PURE__ */ jsxs4("div", { className: "border-b border-panel-border", children: [
1311
- /* @__PURE__ */ jsxs4(
1312
- "button",
1313
- {
1314
- type: "button",
1315
- onClick: () => setOpen((o) => !o),
1316
- 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",
1317
- children: [
1318
- /* @__PURE__ */ jsx5(ChevronRight, { className: cn("size-3.5 transition-transform", open && "rotate-90") }),
1319
- title
1320
- ]
1321
- }
1322
- ),
1323
- /* @__PURE__ */ jsx5("div", { className: cn(!open && "hidden"), children })
1324
- ] });
1325
- }
1326
3532
  function LeftSidebar({
1327
3533
  view,
1328
3534
  onViewChange,
1329
3535
  markLayersSource,
1330
- selectedImageSrc,
1331
- onChooseImage
3536
+ blockSearchRef
1332
3537
  }) {
1333
- const editor = useEditorMaybe();
3538
+ const editor = useEditorMaybe2();
1334
3539
  const labels = useLabels();
1335
- const [layersExpanded, setLayersExpanded] = useState3(false);
1336
- const [blockSearch, setBlockSearch] = useState3("");
1337
- const changeView = (v) => {
1338
- if (v === "settings" && editor && !editor.getSelected()) {
1339
- const wrapper = editor.getWrapper();
1340
- const body = wrapper?.find("mj-body")[0] ?? wrapper;
1341
- if (body) editor.select(body);
1342
- }
1343
- onViewChange(v);
3540
+ const [layersExpanded, setLayersExpanded] = useState5(false);
3541
+ const [blockSearch, setBlockSearch] = useState5("");
3542
+ const [noBlockMatches, setNoBlockMatches] = useState5(false);
3543
+ const selectBody = () => {
3544
+ let body;
3545
+ editor?.getWrapper()?.onAll((c) => {
3546
+ if (!body && tagOf5(c) === "mj-body") body = c;
3547
+ });
3548
+ if (body) editor?.select(body);
1344
3549
  };
1345
3550
  const toggleLayers = (expand) => {
1346
3551
  setLayersExpanded(expand);
@@ -1361,17 +3566,22 @@ function LeftSidebar({
1361
3566
  );
1362
3567
  cat.style.display = anyVisible ? "" : "none";
1363
3568
  });
3569
+ setNoBlockMatches(
3570
+ [...root.querySelectorAll(".gjs-block")].every(
3571
+ (b) => b.style.display === "none"
3572
+ )
3573
+ );
1364
3574
  };
1365
- 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: [
1366
- /* @__PURE__ */ jsx5("div", { className: "flex border-b border-panel-border", children: [
1367
- ["blocks", "Blocks"],
1368
- ["settings", "Settings"],
1369
- ["layers", "Layers"]
1370
- ].map(([value, label]) => /* @__PURE__ */ jsx5(
3575
+ 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: [
3576
+ /* @__PURE__ */ jsx8("div", { className: "flex border-b border-panel-border", children: [
3577
+ ["blocks", labels.tabBlocks],
3578
+ ["settings", labels.tabSettings],
3579
+ ["layers", labels.tabLayers]
3580
+ ].map(([value, label]) => /* @__PURE__ */ jsx8(
1371
3581
  "button",
1372
3582
  {
1373
3583
  type: "button",
1374
- onClick: () => changeView(value),
3584
+ onClick: () => onViewChange(value),
1375
3585
  className: cn(
1376
3586
  "relative flex-1 px-3 py-2.5 text-sm transition-colors",
1377
3587
  "after:absolute after:inset-x-0 after:-bottom-px after:h-0.5 after:transition-colors",
@@ -1381,70 +3591,57 @@ function LeftSidebar({
1381
3591
  },
1382
3592
  value
1383
3593
  )) }),
1384
- /* @__PURE__ */ jsxs4("div", { className: cn("flex min-h-0 flex-1 flex-col", view !== "blocks" && "hidden"), children: [
1385
- /* @__PURE__ */ jsxs4("div", { className: "relative border-b border-panel-border p-2", children: [
1386
- /* @__PURE__ */ jsx5(Search, { className: "pointer-events-none absolute top-1/2 left-4 size-3.5 -translate-y-1/2 text-panel-muted-fg" }),
1387
- /* @__PURE__ */ jsx5(
3594
+ /* @__PURE__ */ jsxs7("div", { className: cn("flex min-h-0 flex-1 flex-col", view !== "blocks" && "hidden"), children: [
3595
+ /* @__PURE__ */ jsxs7("div", { className: "relative border-b border-panel-border p-2", children: [
3596
+ /* @__PURE__ */ jsx8(Search, { className: "pointer-events-none absolute top-1/2 left-4 size-3.5 -translate-y-1/2 text-panel-muted-fg" }),
3597
+ /* @__PURE__ */ jsx8(
1388
3598
  Input,
1389
3599
  {
3600
+ ref: blockSearchRef,
1390
3601
  value: blockSearch,
1391
3602
  onChange: (e) => filterBlocks(e.target.value),
1392
3603
  placeholder: labels.searchBlocks,
1393
3604
  className: "h-8 border-panel-border bg-panel-elevated pr-10 pl-7 text-sm text-panel-fg placeholder:text-panel-muted-fg"
1394
3605
  }
1395
3606
  ),
1396
- /* @__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" })
3607
+ /* @__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: "/" })
1397
3608
  ] }),
1398
- /* @__PURE__ */ jsx5("div", { id: "gjs-blocks", className: "min-h-0 flex-1 overflow-y-auto" })
3609
+ noBlockMatches && /* @__PURE__ */ jsx8("p", { className: "px-3 py-6 text-center text-xs text-panel-muted-fg", children: labels.noBlocksMatch }),
3610
+ /* @__PURE__ */ jsx8("div", { id: "gjs-blocks", className: "min-h-0 flex-1 overflow-y-auto" })
1399
3611
  ] }),
1400
- /* @__PURE__ */ jsxs4("div", { className: cn("min-h-0 flex-1 overflow-y-auto", view !== "settings" && "hidden"), children: [
1401
- selectedImageSrc !== null && /* @__PURE__ */ jsxs4("div", { className: "border-b border-panel-border p-3", children: [
1402
- /* @__PURE__ */ jsxs4(
1403
- "button",
1404
- {
1405
- type: "button",
1406
- onClick: onChooseImage,
1407
- title: labels.chooseImage,
1408
- className: "group relative block w-full overflow-hidden rounded-lg border border-panel-border bg-panel-elevated",
1409
- children: [
1410
- selectedImageSrc ? /* @__PURE__ */ jsx5(
1411
- "img",
1412
- {
1413
- src: selectedImageSrc,
1414
- alt: "",
1415
- className: "h-32 w-full bg-black/25 object-contain"
1416
- }
1417
- ) : /* @__PURE__ */ jsx5("span", { className: "flex h-32 items-center justify-center text-panel-muted-fg", children: /* @__PURE__ */ jsx5(ImageIcon2, { className: "size-8" }) }),
1418
- /* @__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 })
1419
- ]
1420
- }
1421
- ),
1422
- /* @__PURE__ */ jsxs4(
1423
- Button,
1424
- {
1425
- type: "button",
1426
- size: "sm",
1427
- className: "mt-2 w-full bg-brand text-brand-fg hover:bg-brand/90",
1428
- onClick: onChooseImage,
1429
- children: [
1430
- /* @__PURE__ */ jsx5(ImageUp2, {}),
1431
- labels.chooseImage
1432
- ]
1433
- }
1434
- )
1435
- ] }),
1436
- /* @__PURE__ */ jsx5(CollapseSection, { title: labels.attributes, children: /* @__PURE__ */ jsx5("div", { id: "gjs-traits" }) }),
1437
- /* @__PURE__ */ jsx5(CollapseSection, { title: labels.style, children: /* @__PURE__ */ jsx5("div", { id: "gjs-styles" }) })
1438
- ] }),
1439
- /* @__PURE__ */ jsxs4(
3612
+ /* @__PURE__ */ jsx8("div", { className: cn("min-h-0 flex-1 overflow-y-auto", view !== "settings" && "hidden"), children: /* @__PURE__ */ jsx8(
3613
+ Inspector,
3614
+ {
3615
+ onEmailSettings: selectBody,
3616
+ empty: /* @__PURE__ */ jsxs7("div", { className: "flex flex-col items-center gap-3 px-6 py-10 text-center", children: [
3617
+ /* @__PURE__ */ jsx8(MousePointerClick, { className: "size-6 text-panel-muted-fg" }),
3618
+ /* @__PURE__ */ jsx8("p", { className: "text-sm text-panel-muted-fg", children: labels.settingsEmpty }),
3619
+ /* @__PURE__ */ jsxs7(
3620
+ Button,
3621
+ {
3622
+ type: "button",
3623
+ size: "sm",
3624
+ variant: "outline",
3625
+ className: "border-panel-border bg-panel-elevated text-panel-fg hover:bg-panel-hover hover:text-panel-fg",
3626
+ onClick: selectBody,
3627
+ children: [
3628
+ /* @__PURE__ */ jsx8(Mail, {}),
3629
+ labels.emailSettings
3630
+ ]
3631
+ }
3632
+ )
3633
+ ] })
3634
+ }
3635
+ ) }),
3636
+ /* @__PURE__ */ jsxs7(
1440
3637
  "div",
1441
3638
  {
1442
3639
  className: cn("flex min-h-0 flex-1 flex-col", view !== "layers" && "hidden"),
1443
3640
  onMouseDownCapture: markLayersSource,
1444
3641
  children: [
1445
- /* @__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: [
1446
- /* @__PURE__ */ jsx5("span", { children: labels.expandAll }),
1447
- /* @__PURE__ */ jsx5(
3642
+ /* @__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: [
3643
+ /* @__PURE__ */ jsx8("span", { children: labels.expandAll }),
3644
+ /* @__PURE__ */ jsx8(
1448
3645
  Switch,
1449
3646
  {
1450
3647
  checked: layersExpanded,
@@ -1453,7 +3650,16 @@ function LeftSidebar({
1453
3650
  }
1454
3651
  )
1455
3652
  ] }),
1456
- /* @__PURE__ */ jsx5("div", { id: "gjs-layers", className: "min-h-0 flex-1 overflow-y-auto" })
3653
+ /* @__PURE__ */ jsx8(
3654
+ "div",
3655
+ {
3656
+ id: "gjs-layers",
3657
+ className: "min-h-0 flex-1 overflow-y-auto",
3658
+ onDoubleClickCapture: (e) => {
3659
+ if (e.target.closest("[data-name]")) e.stopPropagation();
3660
+ }
3661
+ }
3662
+ )
1457
3663
  ]
1458
3664
  }
1459
3665
  )
@@ -1464,9 +3670,9 @@ function Breadcrumb({
1464
3670
  onSelect
1465
3671
  }) {
1466
3672
  const labels = useLabels();
1467
- 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: [
1468
- i > 0 && /* @__PURE__ */ jsx5(ChevronRight, { className: "size-3 opacity-40" }),
1469
- /* @__PURE__ */ jsx5(
3673
+ 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: [
3674
+ i > 0 && /* @__PURE__ */ jsx8(ChevronRight3, { className: "size-3 opacity-40" }),
3675
+ /* @__PURE__ */ jsx8(
1470
3676
  "button",
1471
3677
  {
1472
3678
  type: "button",
@@ -1475,7 +3681,7 @@ function Breadcrumb({
1475
3681
  "rounded px-1.5 py-0.5 hover:bg-muted hover:text-foreground",
1476
3682
  i === crumbs.length - 1 && "font-medium text-brand"
1477
3683
  ),
1478
- children: crumbName(c)
3684
+ children: crumbName(c, labels)
1479
3685
  }
1480
3686
  )
1481
3687
  ] }, i)) });