@mjml-agent-editor/editor 0.1.12 → 0.1.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/canvas.cjs CHANGED
@@ -37,9 +37,9 @@ module.exports = __toCommonJS(canvas_exports);
37
37
 
38
38
  // src/components/editor/EmailEditor.tsx
39
39
  var import_grapesjs = __toESM(require("grapesjs"), 1);
40
- var import_react4 = __toESM(require("@grapesjs/react"), 1);
40
+ var import_react6 = __toESM(require("@grapesjs/react"), 1);
41
41
  var import_grapesjs_mjml = __toESM(require("grapesjs-mjml"), 1);
42
- var import_react5 = require("react");
42
+ var import_react7 = require("react");
43
43
  var import_core2 = require("@mjml-agent-editor/core");
44
44
 
45
45
  // src/stores.tsx
@@ -74,6 +74,29 @@ var DEFAULT_LABELS = {
74
74
  rteBackgroundColor: "Background color",
75
75
  documentLoadFailed: "Failed to load the document.",
76
76
  documentSaveFailed: "Failed to save changes.",
77
+ retry: "Retry",
78
+ chooseImage: "Choose image",
79
+ imagePickerTitle: "Choose an image",
80
+ imagePickerDrop: "Drag images here, or",
81
+ imagePickerBrowse: "Browse files",
82
+ imagePickerUrlPlaceholder: "\u2026or paste an image URL",
83
+ imagePickerUseUrl: "Use URL",
84
+ imagePickerGallery: "Your images",
85
+ imagePickerEmpty: "No images yet \u2014 the ones you upload will be kept here.",
86
+ imagePickerQuota: (used, limit) => `${used} of ${limit} uploads used`,
87
+ imagePickerQuotaFull: "Upload limit reached \u2014 pick an image below or paste a URL.",
88
+ imagePickerDelete: "Delete image",
89
+ imagePickerConfirmDelete: "Click again to delete",
90
+ imagePickerLoadFailed: "Failed to load your images.",
91
+ imageUploadFailed: "Failed to upload the image.",
92
+ imageDeleteFailed: "Failed to delete the image.",
93
+ imagePickerUnsupported: (fileName) => `${fileName} is not a supported image type.`,
94
+ imageFieldRemove: "Remove image",
95
+ colorPickerOpen: "Pick a color",
96
+ colorPickerEyedropper: "Pick from the screen",
97
+ colorPickerDocument: "In this email",
98
+ colorPickerPresets: "Presets",
99
+ colorPickerTransparent: "Transparent",
77
100
  commentOnSection: "Comment on the whole section",
78
101
  closeComments: "Close",
79
102
  noCommentsYet: "No comments yet \u2014 add the first one.",
@@ -85,6 +108,7 @@ var DEFAULT_LABELS = {
85
108
  agentWorking: "Agent is working\u2026",
86
109
  reasoning: "Reasoning",
87
110
  completedSteps: (count, failed) => failed > 0 ? `Completed steps (${count}, ${failed} failed)` : `Completed steps (${count})`,
111
+ stepLimitReached: "The agent ran out of steps for this turn. Ask it to continue.",
88
112
  applyComments: "Apply changes from comments",
89
113
  chatPlaceholder: "Describe what you want to change\u2026 (Enter = send, Shift+Enter = new line)",
90
114
  stop: "Stop",
@@ -115,6 +139,17 @@ function mergeLabels(overrides) {
115
139
  var import_jsx_runtime = require("react/jsx-runtime");
116
140
  var StoreContext = (0, import_react.createContext)(null);
117
141
  var LabelContext = (0, import_react.createContext)(null);
142
+ var TurnContext = (0, import_react.createContext)({
143
+ running: false,
144
+ setRunning: () => {
145
+ },
146
+ locked: false,
147
+ setLocked: () => {
148
+ }
149
+ });
150
+ function useAgentTurn() {
151
+ return (0, import_react.useContext)(TurnContext);
152
+ }
118
153
  function useLabels() {
119
154
  return (0, import_react.useContext)(LabelContext) ?? mergeLabels();
120
155
  }
@@ -133,6 +168,9 @@ function useDocumentStore() {
133
168
  function useCommentStore() {
134
169
  return useStores().comments;
135
170
  }
171
+ function useImageLibrary() {
172
+ return useStores().images;
173
+ }
136
174
 
137
175
  // src/index.ts
138
176
  var import_core = require("@mjml-agent-editor/core");
@@ -239,6 +277,9 @@ var import_jsx_runtime5 = require("react/jsx-runtime");
239
277
  function Popover({ ...props }) {
240
278
  return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_radix_ui2.Popover.Root, { "data-slot": "popover", ...props });
241
279
  }
280
+ function PopoverTrigger({ ...props }) {
281
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_radix_ui2.Popover.Trigger, { "data-slot": "popover-trigger", ...props });
282
+ }
242
283
  function PopoverAnchor({ ...props }) {
243
284
  return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_radix_ui2.Popover.Anchor, { "data-slot": "popover-anchor", ...props });
244
285
  }
@@ -263,11 +304,117 @@ function PopoverContent({
263
304
  ) });
264
305
  }
265
306
 
266
- // src/components/ui/textarea.tsx
307
+ // src/components/ui/dialog.tsx
267
308
  var React4 = require("react");
309
+ var import_radix_ui3 = require("radix-ui");
310
+ var import_lucide_react2 = require("lucide-react");
268
311
  var import_jsx_runtime6 = require("react/jsx-runtime");
269
- function Textarea({ className, ...props }) {
312
+ function Dialog({ ...props }) {
313
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_radix_ui3.Dialog.Root, { "data-slot": "dialog", ...props });
314
+ }
315
+ function DialogPortal({ ...props }) {
316
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_radix_ui3.Dialog.Portal, { "data-slot": "dialog-portal", ...props });
317
+ }
318
+ function DialogOverlay({
319
+ className,
320
+ ...props
321
+ }) {
322
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
323
+ import_radix_ui3.Dialog.Overlay,
324
+ {
325
+ "data-slot": "dialog-overlay",
326
+ className: cn(
327
+ "fixed inset-0 isolate z-50 bg-black/10 duration-100 supports-backdrop-filter:backdrop-blur-xs data-open:animate-in data-open:fade-in-0 data-closed:animate-out data-closed:fade-out-0",
328
+ className
329
+ ),
330
+ ...props
331
+ }
332
+ );
333
+ }
334
+ function DialogContent({
335
+ className,
336
+ children,
337
+ showCloseButton = true,
338
+ ...props
339
+ }) {
340
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(DialogPortal, { children: [
341
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(DialogOverlay, {}),
342
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
343
+ import_radix_ui3.Dialog.Content,
344
+ {
345
+ "data-slot": "dialog-content",
346
+ className: cn(
347
+ "fixed top-1/2 left-1/2 z-50 grid w-full max-w-[calc(100%-2rem)] -translate-x-1/2 -translate-y-1/2 gap-4 rounded-xl bg-popover p-4 text-sm text-popover-foreground ring-1 ring-foreground/10 duration-100 outline-none sm:max-w-sm data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
348
+ className
349
+ ),
350
+ ...props,
351
+ children: [
352
+ children,
353
+ showCloseButton && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_radix_ui3.Dialog.Close, { "data-slot": "dialog-close", asChild: true, children: /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(Button, { variant: "ghost", className: "absolute top-2 right-2", size: "icon-sm", children: [
354
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_lucide_react2.XIcon, {}),
355
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "sr-only", children: "Close" })
356
+ ] }) })
357
+ ]
358
+ }
359
+ )
360
+ ] });
361
+ }
362
+ function DialogHeader({ className, ...props }) {
363
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { "data-slot": "dialog-header", className: cn("flex flex-col gap-2", className), ...props });
364
+ }
365
+ function DialogTitle({ className, ...props }) {
366
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
367
+ import_radix_ui3.Dialog.Title,
368
+ {
369
+ "data-slot": "dialog-title",
370
+ className: cn("font-heading text-base leading-none font-medium", className),
371
+ ...props
372
+ }
373
+ );
374
+ }
375
+ function DialogDescription({
376
+ className,
377
+ ...props
378
+ }) {
270
379
  return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
380
+ import_radix_ui3.Dialog.Description,
381
+ {
382
+ "data-slot": "dialog-description",
383
+ className: cn(
384
+ "text-sm text-muted-foreground *:[a]:underline *:[a]:underline-offset-3 *:[a]:hover:text-foreground",
385
+ className
386
+ ),
387
+ ...props
388
+ }
389
+ );
390
+ }
391
+
392
+ // src/lib/refusal.ts
393
+ function refusal(error) {
394
+ if (!(error instanceof Error)) return null;
395
+ const status = error.status;
396
+ const deliberate = typeof status === "number" ? status >= 400 && status < 500 : null;
397
+ if (deliberate === false) return null;
398
+ if (error.message.startsWith("{")) {
399
+ try {
400
+ const parsed = JSON.parse(error.message);
401
+ return typeof parsed.error === "string" && parsed.error.trim() !== "" ? parsed.error : null;
402
+ } catch {
403
+ return null;
404
+ }
405
+ }
406
+ const message = error.message.trim();
407
+ return deliberate && message !== "" && !message.startsWith("<") ? error.message : null;
408
+ }
409
+ function saveToastId(docId) {
410
+ return `document-save-${docId}`;
411
+ }
412
+
413
+ // src/components/ui/textarea.tsx
414
+ var React5 = require("react");
415
+ var import_jsx_runtime7 = require("react/jsx-runtime");
416
+ function Textarea({ className, ...props }) {
417
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
271
418
  "textarea",
272
419
  {
273
420
  "data-slot": "textarea",
@@ -283,8 +430,8 @@ function Textarea({ className, ...props }) {
283
430
  // src/components/comments/CanvasComments.tsx
284
431
  var import_react2 = require("@grapesjs/react");
285
432
  var import_react3 = require("react");
286
- var import_lucide_react2 = require("lucide-react");
287
- var import_jsx_runtime7 = require("react/jsx-runtime");
433
+ var import_lucide_react3 = require("lucide-react");
434
+ var import_jsx_runtime8 = require("react/jsx-runtime");
288
435
  var keyOf = (c) => c.objectId ? `obj-${c.objectId}` : `sec-${c.sectionId}`;
289
436
  var keyOfTarget = (t) => t.objectId ? `obj-${t.objectId}` : `sec-${t.sectionId}`;
290
437
  function CanvasComments({
@@ -438,11 +585,11 @@ function CanvasComments({
438
585
  console.error(e);
439
586
  }
440
587
  };
441
- return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { ref: containerRef, className: "pointer-events-none absolute inset-0 z-20 overflow-hidden", children: [
588
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { ref: containerRef, className: "pointer-events-none absolute inset-0 z-20 overflow-hidden", children: [
442
589
  pins.map(({ key, list }) => {
443
590
  const pos = positions[key];
444
591
  if (!pos) return null;
445
- return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
592
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
446
593
  "button",
447
594
  {
448
595
  type: "button",
@@ -458,8 +605,8 @@ function CanvasComments({
458
605
  key
459
606
  );
460
607
  }),
461
- /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(Popover, { open: !!activeKey && !!activePos, onOpenChange: (o) => !o && closePopover(), children: [
462
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(PopoverAnchor, { asChild: true, children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
608
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(Popover, { open: !!activeKey && !!activePos, onOpenChange: (o) => !o && closePopover(), children: [
609
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(PopoverAnchor, { asChild: true, children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
463
610
  "div",
464
611
  {
465
612
  style: {
@@ -471,45 +618,45 @@ function CanvasComments({
471
618
  }
472
619
  }
473
620
  ) }),
474
- /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(
621
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
475
622
  PopoverContent,
476
623
  {
477
624
  side: "right",
478
625
  align: "start",
479
626
  className: "pointer-events-auto w-80 p-0 data-[state=closed]:!animate-none data-[state=closed]:!duration-0",
480
627
  children: [
481
- /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "flex items-start justify-between gap-2 border-b px-3 py-2 text-xs text-muted-foreground", children: [
482
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { className: "min-w-0", children: activeTarget?.objectId ? /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(import_jsx_runtime7.Fragment, { children: [
628
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "flex items-start justify-between gap-2 border-b px-3 py-2 text-xs text-muted-foreground", children: [
629
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { className: "min-w-0", children: activeTarget?.objectId ? /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(import_jsx_runtime8.Fragment, { children: [
483
630
  "Comment on",
484
631
  " ",
485
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { className: "font-medium text-foreground", children: activeTarget.objectLabel })
486
- ] }) : /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(import_jsx_runtime7.Fragment, { children: [
632
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { className: "font-medium text-foreground", children: activeTarget.objectLabel })
633
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(import_jsx_runtime8.Fragment, { children: [
487
634
  labels.commentOnSection,
488
635
  " ",
489
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("code", { className: "font-mono", children: activeTarget?.sectionId })
636
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("code", { className: "font-mono", children: activeTarget?.sectionId })
490
637
  ] }) }),
491
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
638
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
492
639
  "button",
493
640
  {
494
641
  type: "button",
495
642
  onClick: closePopover,
496
643
  "aria-label": labels.closeComments,
497
644
  className: "-mr-1 -mt-0.5 shrink-0 rounded p-0.5 text-muted-foreground hover:bg-muted hover:text-foreground",
498
- children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_lucide_react2.X, { className: "size-4" })
645
+ children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_lucide_react3.X, { className: "size-4" })
499
646
  }
500
647
  )
501
648
  ] }),
502
- /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "max-h-64 space-y-2 overflow-y-auto p-3", children: [
503
- activeComments.length === 0 && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("p", { className: "text-xs text-muted-foreground", children: labels.noCommentsYet }),
504
- activeComments.map((c) => /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "rounded-md border bg-muted/40 p-2 text-sm", children: [
505
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("p", { className: "whitespace-pre-wrap", children: c.body }),
506
- /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(
649
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "max-h-64 space-y-2 overflow-y-auto p-3", children: [
650
+ activeComments.length === 0 && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("p", { className: "text-xs text-muted-foreground", children: labels.noCommentsYet }),
651
+ activeComments.map((c) => /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "rounded-md border bg-muted/40 p-2 text-sm", children: [
652
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("p", { className: "whitespace-pre-wrap", children: c.body }),
653
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
507
654
  "button",
508
655
  {
509
656
  onClick: () => void resolve(c.id),
510
657
  className: "mt-1 inline-flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground",
511
658
  children: [
512
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_lucide_react2.Check, { className: "size-3" }),
659
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_lucide_react3.Check, { className: "size-3" }),
513
660
  " ",
514
661
  labels.resolveComment
515
662
  ]
@@ -517,8 +664,8 @@ function CanvasComments({
517
664
  )
518
665
  ] }, c.id))
519
666
  ] }),
520
- /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "border-t p-3", children: [
521
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
667
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "border-t p-3", children: [
668
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
522
669
  Textarea,
523
670
  {
524
671
  autoFocus: true,
@@ -534,8 +681,8 @@ function CanvasComments({
534
681
  }
535
682
  }
536
683
  ),
537
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { className: "mt-2 flex justify-end", children: /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(Button, { size: "sm", disabled: !body.trim(), onClick: () => void submit(), children: [
538
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_lucide_react2.MessageSquarePlus, {}),
684
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { className: "mt-2 flex justify-end", children: /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(Button, { size: "sm", disabled: !body.trim(), onClick: () => void submit(), children: [
685
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_lucide_react3.MessageSquarePlus, {}),
539
686
  " ",
540
687
  labels.addComment
541
688
  ] }) })
@@ -547,21 +694,50 @@ function CanvasComments({
547
694
  ] });
548
695
  }
549
696
 
697
+ // src/lib/save-queue.ts
698
+ function createSaveQueue(save, delayMs) {
699
+ let timer = null;
700
+ let tail = Promise.resolve();
701
+ const cancel = () => {
702
+ if (timer) clearTimeout(timer);
703
+ timer = null;
704
+ };
705
+ const run = (task) => {
706
+ const next = tail.then(task);
707
+ tail = next.catch(() => {
708
+ });
709
+ return next;
710
+ };
711
+ return {
712
+ schedule() {
713
+ cancel();
714
+ timer = setTimeout(() => void run(save).catch(() => {
715
+ }), delayMs);
716
+ },
717
+ cancel,
718
+ flush() {
719
+ cancel();
720
+ return run(save);
721
+ },
722
+ run
723
+ };
724
+ }
725
+
550
726
  // src/components/editor/EmailEditor.tsx
551
- var import_sonner = require("sonner");
552
- var import_lucide_react3 = require("lucide-react");
727
+ var import_sonner2 = require("sonner");
728
+ var import_lucide_react7 = require("lucide-react");
553
729
 
554
730
  // src/components/ui/switch.tsx
555
- var React5 = require("react");
556
- var import_radix_ui3 = require("radix-ui");
557
- var import_jsx_runtime8 = require("react/jsx-runtime");
731
+ var React6 = require("react");
732
+ var import_radix_ui4 = require("radix-ui");
733
+ var import_jsx_runtime9 = require("react/jsx-runtime");
558
734
  function Switch({
559
735
  className,
560
736
  size = "default",
561
737
  ...props
562
738
  }) {
563
- return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
564
- import_radix_ui3.Switch.Root,
739
+ return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
740
+ import_radix_ui4.Switch.Root,
565
741
  {
566
742
  "data-slot": "switch",
567
743
  "data-size": size,
@@ -570,8 +746,8 @@ function Switch({
570
746
  className
571
747
  ),
572
748
  ...props,
573
- children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
574
- import_radix_ui3.Switch.Thumb,
749
+ children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
750
+ import_radix_ui4.Switch.Thumb,
575
751
  {
576
752
  "data-slot": "switch-thumb",
577
753
  className: "pointer-events-none block rounded-full bg-background ring-0 transition-transform group-data-[size=default]/switch:size-4 group-data-[size=sm]/switch:size-3 group-data-[size=default]/switch:data-checked:translate-x-[calc(100%-2px)] group-data-[size=sm]/switch:data-checked:translate-x-[calc(100%-2px)] dark:data-checked:bg-primary-foreground group-data-[size=default]/switch:data-unchecked:translate-x-0 group-data-[size=sm]/switch:data-unchecked:translate-x-0 dark:data-unchecked:bg-foreground"
@@ -582,8 +758,583 @@ function Switch({
582
758
  }
583
759
 
584
760
  // src/components/editor/EmailEditor.tsx
761
+ var import_lucide_react8 = require("lucide-react");
762
+
763
+ // src/components/editor/ImagePicker.tsx
764
+ var import_react4 = require("react");
585
765
  var import_lucide_react4 = require("lucide-react");
586
- var import_jsx_runtime9 = require("react/jsx-runtime");
766
+ var import_sonner = require("sonner");
767
+ var import_jsx_runtime10 = require("react/jsx-runtime");
768
+ function ImagePicker({
769
+ open,
770
+ currentUrl,
771
+ onSelect,
772
+ onClose
773
+ }) {
774
+ const library = useImageLibrary();
775
+ const labels = useLabels();
776
+ const [images, setImages] = (0, import_react4.useState)([]);
777
+ const [quota, setQuota] = (0, import_react4.useState)();
778
+ const [loading, setLoading] = (0, import_react4.useState)(false);
779
+ const [pending, setPending] = (0, import_react4.useState)(0);
780
+ const [dragging, setDragging] = (0, import_react4.useState)(false);
781
+ const [url, setUrl] = (0, import_react4.useState)("");
782
+ const [confirmId, setConfirmId] = (0, import_react4.useState)(null);
783
+ const fileInput = (0, import_react4.useRef)(null);
784
+ const session = (0, import_react4.useRef)(0);
785
+ const latest = (0, import_react4.useRef)({ labels, onSelect });
786
+ latest.current = { labels, onSelect };
787
+ const lists = (0, import_react4.useRef)(0);
788
+ const refresh = (0, import_react4.useCallback)(async () => {
789
+ if (!library) return;
790
+ const mine = ++lists.current;
791
+ setLoading(true);
792
+ try {
793
+ const result = await library.list();
794
+ if (mine !== lists.current) return;
795
+ setImages(result.images);
796
+ setQuota(result.quota);
797
+ } catch (err) {
798
+ console.error(err);
799
+ if (mine === lists.current) import_sonner.toast.error(latest.current.labels.imagePickerLoadFailed);
800
+ } finally {
801
+ if (mine === lists.current) setLoading(false);
802
+ }
803
+ }, [library]);
804
+ (0, import_react4.useEffect)(() => {
805
+ session.current += 1;
806
+ if (!open) return;
807
+ setUrl("");
808
+ setConfirmId(null);
809
+ void refresh();
810
+ }, [open]);
811
+ (0, import_react4.useEffect)(() => () => void (session.current += 1), []);
812
+ const accepts = (file) => matchesAccept(file, library?.accept);
813
+ const full = quota !== void 0 && quota.used >= quota.limit;
814
+ const upload = async (files) => {
815
+ if (!library || !files?.length) return;
816
+ const mine = session.current;
817
+ const list = Array.from(files).filter((f) => {
818
+ if (accepts(f)) return true;
819
+ import_sonner.toast.error(labels.imagePickerUnsupported(f.name));
820
+ return false;
821
+ });
822
+ if (!list.length) return;
823
+ setPending((n) => n + list.length);
824
+ const results = await Promise.allSettled(list.map(async (f) => library.upload(f)));
825
+ setPending((n) => n - list.length);
826
+ for (const r of results) {
827
+ if (r.status === "rejected") {
828
+ console.error(r.reason);
829
+ import_sonner.toast.error(r.reason instanceof Error ? r.reason.message : labels.imageUploadFailed);
830
+ }
831
+ }
832
+ if (session.current !== mine) return;
833
+ await refresh();
834
+ const [only] = results;
835
+ if (results.length === 1 && only?.status === "fulfilled" && session.current === mine) {
836
+ latest.current.onSelect(only.value.url);
837
+ }
838
+ };
839
+ const remove = async (image) => {
840
+ if (!library?.remove) return;
841
+ if (confirmId !== image.id) {
842
+ setConfirmId(image.id);
843
+ return;
844
+ }
845
+ setConfirmId(null);
846
+ try {
847
+ await library.remove(image.id);
848
+ await refresh();
849
+ } catch (err) {
850
+ console.error(err);
851
+ import_sonner.toast.error(err instanceof Error ? err.message : labels.imageDeleteFailed);
852
+ }
853
+ };
854
+ const onDrop = (e) => {
855
+ e.preventDefault();
856
+ setDragging(false);
857
+ if (!full) void upload(e.dataTransfer.files);
858
+ };
859
+ return /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(Dialog, { open, onOpenChange: (next) => !next && onClose(), children: /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(DialogContent, { className: "editor-dark gap-0 overflow-hidden border border-panel-border bg-panel p-0 text-panel-fg ring-0 sm:max-w-3xl", children: [
860
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(DialogHeader, { className: "border-b border-panel-border px-5 py-4", children: [
861
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(DialogTitle, { children: labels.imagePickerTitle }),
862
+ quota && /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(DialogDescription, { className: "text-panel-muted-fg", children: labels.imagePickerQuota(quota.used, quota.limit) })
863
+ ] }),
864
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("div", { className: "flex max-h-[70vh] flex-col gap-4 overflow-y-auto p-5", children: [
865
+ library && (full ? /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("p", { className: "rounded-lg border border-panel-border bg-panel-elevated px-4 py-3 text-sm text-panel-muted-fg", children: labels.imagePickerQuotaFull }) : /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(
866
+ "div",
867
+ {
868
+ onDragOver: (e) => {
869
+ e.preventDefault();
870
+ setDragging(true);
871
+ },
872
+ onDragLeave: () => setDragging(false),
873
+ onDrop,
874
+ className: cn(
875
+ "flex flex-col items-center justify-center gap-3 rounded-xl border-2 border-dashed px-6 py-8 text-center transition-colors",
876
+ dragging ? "border-brand bg-brand-soft" : "border-panel-border bg-panel-elevated"
877
+ ),
878
+ children: [
879
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_lucide_react4.ImageUp, { className: "size-8 text-panel-muted-fg" }),
880
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("div", { className: "flex flex-wrap items-center justify-center gap-2 text-sm text-panel-muted-fg", children: [
881
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("span", { children: labels.imagePickerDrop }),
882
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
883
+ Button,
884
+ {
885
+ type: "button",
886
+ size: "sm",
887
+ className: "bg-brand text-brand-fg hover:bg-brand/90",
888
+ onClick: () => fileInput.current?.click(),
889
+ children: labels.imagePickerBrowse
890
+ }
891
+ )
892
+ ] }),
893
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
894
+ "input",
895
+ {
896
+ ref: fileInput,
897
+ type: "file",
898
+ accept: library.accept ?? "image/*",
899
+ multiple: true,
900
+ hidden: true,
901
+ onChange: (e) => {
902
+ void upload(e.target.files);
903
+ e.target.value = "";
904
+ }
905
+ }
906
+ )
907
+ ]
908
+ }
909
+ )),
910
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(
911
+ "form",
912
+ {
913
+ className: "flex gap-2",
914
+ onSubmit: (e) => {
915
+ e.preventDefault();
916
+ const value = url.trim();
917
+ if (value) onSelect(value);
918
+ },
919
+ children: [
920
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("div", { className: "relative flex-1", children: [
921
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_lucide_react4.Link2, { className: "pointer-events-none absolute top-1/2 left-2.5 size-4 -translate-y-1/2 text-panel-muted-fg" }),
922
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
923
+ Input,
924
+ {
925
+ value: url,
926
+ onChange: (e) => setUrl(e.target.value),
927
+ placeholder: labels.imagePickerUrlPlaceholder,
928
+ className: "h-9 border-panel-border bg-panel-elevated pl-8 text-panel-fg placeholder:text-panel-muted-fg"
929
+ }
930
+ )
931
+ ] }),
932
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
933
+ Button,
934
+ {
935
+ type: "submit",
936
+ size: "lg",
937
+ variant: "outline",
938
+ disabled: !url.trim(),
939
+ className: "border-panel-border bg-panel-elevated text-panel-fg hover:bg-panel-hover hover:text-panel-fg",
940
+ children: labels.imagePickerUseUrl
941
+ }
942
+ )
943
+ ]
944
+ }
945
+ ),
946
+ library && /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("section", { children: [
947
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("h3", { className: "mb-2 text-xs font-medium tracking-wide text-panel-muted-fg uppercase", children: labels.imagePickerGallery }),
948
+ loading && images.length === 0 && pending === 0 ? /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("div", { className: "flex justify-center py-8", children: /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(Spinner, {}) }) : images.length === 0 && pending === 0 ? /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("p", { className: "py-6 text-center text-sm text-panel-muted-fg", children: labels.imagePickerEmpty }) : /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("ul", { className: "grid grid-cols-3 gap-3 sm:grid-cols-4", children: [
949
+ Array.from({ length: pending }, (_, i) => /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
950
+ "li",
951
+ {
952
+ className: "flex aspect-[4/3] items-center justify-center rounded-lg border border-panel-border bg-panel-elevated",
953
+ children: /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(Spinner, {})
954
+ },
955
+ `pending-${i}`
956
+ )),
957
+ images.map((image) => {
958
+ const selected = image.url === currentUrl;
959
+ const confirming = confirmId === image.id;
960
+ return /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("li", { className: "group relative", children: [
961
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
962
+ "button",
963
+ {
964
+ type: "button",
965
+ onClick: () => onSelect(image.url),
966
+ title: image.name,
967
+ className: cn(
968
+ "block aspect-[4/3] w-full overflow-hidden rounded-lg border bg-black/25 transition outline-none focus-visible:ring-2 focus-visible:ring-brand",
969
+ selected ? "border-brand ring-2 ring-brand" : "border-panel-border hover:border-panel-muted-fg"
970
+ ),
971
+ children: /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
972
+ "img",
973
+ {
974
+ src: image.url,
975
+ alt: image.name ?? "",
976
+ loading: "lazy",
977
+ className: "size-full object-contain"
978
+ }
979
+ )
980
+ }
981
+ ),
982
+ selected && /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("span", { className: "pointer-events-none absolute top-1.5 left-1.5 flex size-5 items-center justify-center rounded-full bg-brand text-brand-fg", children: /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_lucide_react4.Check, { className: "size-3" }) }),
983
+ image.name && /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("span", { className: "pointer-events-none absolute inset-x-0 bottom-0 truncate rounded-b-lg bg-black/60 px-2 py-1 text-[11px] text-white opacity-0 transition group-hover:opacity-100", children: image.name }),
984
+ library.remove && /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(
985
+ "button",
986
+ {
987
+ type: "button",
988
+ onClick: () => void remove(image),
989
+ onBlur: () => confirming && setConfirmId(null),
990
+ title: confirming ? labels.imagePickerConfirmDelete : labels.imagePickerDelete,
991
+ "aria-label": labels.imagePickerDelete,
992
+ className: cn(
993
+ "absolute top-1.5 right-1.5 flex h-6 items-center gap-1 rounded-md px-1.5 text-xs text-white transition focus-visible:opacity-100",
994
+ confirming ? "bg-red-600 opacity-100" : "bg-black/60 opacity-0 group-hover:opacity-100 hover:bg-black/80"
995
+ ),
996
+ children: [
997
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_lucide_react4.Trash2, { className: "size-3.5" }),
998
+ confirming && labels.imagePickerConfirmDelete
999
+ ]
1000
+ }
1001
+ )
1002
+ ] }, image.id);
1003
+ })
1004
+ ] })
1005
+ ] })
1006
+ ] })
1007
+ ] }) });
1008
+ }
1009
+ function matchesAccept(file, accept) {
1010
+ if (!accept) return file.type.startsWith("image/");
1011
+ return accept.split(",").some((raw) => {
1012
+ const rule = raw.trim().toLowerCase();
1013
+ if (rule.startsWith(".")) return file.name.toLowerCase().endsWith(rule);
1014
+ if (rule.endsWith("/*")) return file.type.startsWith(rule.slice(0, -1));
1015
+ return file.type === rule;
1016
+ });
1017
+ }
1018
+
1019
+ // src/components/editor/ColorField.tsx
1020
+ var import_react5 = require("react");
1021
+ var import_react_colorful = require("react-colorful");
1022
+ var import_lucide_react5 = require("lucide-react");
1023
+ var import_jsx_runtime11 = require("react/jsx-runtime");
1024
+ var PRESETS = [
1025
+ "#000000",
1026
+ "#374151",
1027
+ "#6b7280",
1028
+ "#d1d5db",
1029
+ "#f3f4f6",
1030
+ "#ffffff",
1031
+ "#dc2626",
1032
+ "#ea580c",
1033
+ "#ca8a04",
1034
+ "#16a34a",
1035
+ "#0284c7",
1036
+ "#4f46e5",
1037
+ "#9333ea",
1038
+ "#db2777"
1039
+ ];
1040
+ var eyeDropper = () => globalThis.EyeDropper;
1041
+ function ColorField({
1042
+ value,
1043
+ placeholder,
1044
+ documentColors,
1045
+ labels,
1046
+ onChange
1047
+ }) {
1048
+ const [text, setText] = (0, import_react5.useState)(value);
1049
+ const [open, setOpen] = (0, import_react5.useState)(false);
1050
+ const [used, setUsed] = (0, import_react5.useState)([]);
1051
+ const pressed = (0, import_react5.useRef)(false);
1052
+ const dragged = (0, import_react5.useRef)(null);
1053
+ const fieldRef = (0, import_react5.useRef)(null);
1054
+ const anchor = (0, import_react5.useRef)({
1055
+ getBoundingClientRect: () => {
1056
+ const field = fieldRef.current?.getBoundingClientRect() ?? new DOMRect();
1057
+ const panel = fieldRef.current?.closest(".editor-dark")?.getBoundingClientRect();
1058
+ return new DOMRect(panel?.right ?? field.right, field.top, 0, field.height);
1059
+ }
1060
+ });
1061
+ (0, import_react5.useEffect)(() => setText(value), [value]);
1062
+ const latestOnChange = (0, import_react5.useRef)(onChange);
1063
+ latestOnChange.current = onChange;
1064
+ const release = (0, import_react5.useCallback)(() => {
1065
+ pressed.current = false;
1066
+ if (dragged.current !== null) latestOnChange.current(dragged.current, false);
1067
+ dragged.current = null;
1068
+ }, []);
1069
+ (0, import_react5.useEffect)(() => {
1070
+ document.addEventListener("lostpointercapture", release);
1071
+ return () => {
1072
+ document.removeEventListener("lostpointercapture", release);
1073
+ release();
1074
+ };
1075
+ }, [release]);
1076
+ const hex = toHex(value) ?? toHex(placeholder ?? "") ?? "#000000";
1077
+ const translucent = hex.length === 9;
1078
+ const empty = !value;
1079
+ const commit = (next) => {
1080
+ dragged.current = null;
1081
+ onChange(next, false);
1082
+ };
1083
+ const drag = (next) => {
1084
+ setText(next);
1085
+ if (!pressed.current) return commit(next);
1086
+ dragged.current = next;
1087
+ onChange(next, true);
1088
+ };
1089
+ const pickFromScreen = async () => {
1090
+ const Ctor = eyeDropper();
1091
+ if (!Ctor) return;
1092
+ try {
1093
+ const { sRGBHex } = await new Ctor().open();
1094
+ commit(sRGBHex);
1095
+ } catch {
1096
+ }
1097
+ };
1098
+ return /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(
1099
+ Popover,
1100
+ {
1101
+ open,
1102
+ onOpenChange: (next) => {
1103
+ if (next) setUsed(documentColors());
1104
+ else if (text.trim() !== value) commit(text.trim());
1105
+ setOpen(next);
1106
+ },
1107
+ children: [
1108
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(PopoverAnchor, { virtualRef: anchor }),
1109
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(
1110
+ "div",
1111
+ {
1112
+ ref: fieldRef,
1113
+ 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",
1114
+ children: [
1115
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
1116
+ "input",
1117
+ {
1118
+ value: text,
1119
+ placeholder,
1120
+ spellCheck: false,
1121
+ onChange: (e) => setText(e.target.value),
1122
+ onBlur: () => text !== value && commit(text.trim()),
1123
+ onKeyDown: (e) => {
1124
+ if (e.key === "Enter") commit(text.trim());
1125
+ if (e.key === "Escape") setText(value);
1126
+ },
1127
+ className: "min-w-0 flex-1 bg-transparent font-mono text-xs text-panel-fg outline-none placeholder:text-panel-muted-fg"
1128
+ }
1129
+ ),
1130
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(PopoverTrigger, { asChild: true, children: /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(
1131
+ "button",
1132
+ {
1133
+ type: "button",
1134
+ title: labels.colorPickerOpen,
1135
+ "aria-label": labels.colorPickerOpen,
1136
+ className: "relative size-5 shrink-0 overflow-hidden rounded border border-white/20 outline-none focus-visible:ring-2 focus-visible:ring-brand",
1137
+ children: [
1138
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(Checkerboard, {}),
1139
+ !empty && /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("span", { className: "absolute inset-0", style: { background: value } })
1140
+ ]
1141
+ }
1142
+ ) })
1143
+ ]
1144
+ }
1145
+ ),
1146
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(
1147
+ PopoverContent,
1148
+ {
1149
+ side: "right",
1150
+ align: "start",
1151
+ sideOffset: 8,
1152
+ className: "editor-dark flex w-52 flex-col gap-2.5 border border-panel-border bg-panel p-2.5 text-panel-fg",
1153
+ children: [
1154
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
1155
+ "div",
1156
+ {
1157
+ onPointerDownCapture: (e) => {
1158
+ pressed.current = true;
1159
+ e.target.setPointerCapture(e.pointerId);
1160
+ },
1161
+ children: translucent ? /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_react_colorful.HexAlphaColorPicker, { color: hex, onChange: drag, className: "color-field-picker" }) : /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_react_colorful.HexColorPicker, { color: hex, onChange: drag, className: "color-field-picker" })
1162
+ }
1163
+ ),
1164
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { className: "flex items-center gap-1.5", children: [
1165
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
1166
+ "span",
1167
+ {
1168
+ className: "size-6 shrink-0 rounded border border-white/20",
1169
+ style: { background: hex }
1170
+ }
1171
+ ),
1172
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
1173
+ "input",
1174
+ {
1175
+ value: text,
1176
+ spellCheck: false,
1177
+ onChange: (e) => setText(e.target.value),
1178
+ onKeyDown: (e) => e.key === "Enter" && commit(text.trim()),
1179
+ onBlur: () => text !== value && commit(text.trim()),
1180
+ 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"
1181
+ }
1182
+ ),
1183
+ eyeDropper() && /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
1184
+ "button",
1185
+ {
1186
+ type: "button",
1187
+ onClick: () => void pickFromScreen(),
1188
+ title: labels.colorPickerEyedropper,
1189
+ "aria-label": labels.colorPickerEyedropper,
1190
+ 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",
1191
+ children: /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_lucide_react5.Pipette, { className: "size-3" })
1192
+ }
1193
+ )
1194
+ ] }),
1195
+ used.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
1196
+ Swatches,
1197
+ {
1198
+ title: labels.colorPickerDocument,
1199
+ colors: used,
1200
+ current: hex,
1201
+ onPick: commit
1202
+ }
1203
+ ),
1204
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
1205
+ Swatches,
1206
+ {
1207
+ title: labels.colorPickerPresets,
1208
+ colors: PRESETS,
1209
+ current: hex,
1210
+ onPick: commit,
1211
+ transparentLabel: labels.colorPickerTransparent
1212
+ }
1213
+ )
1214
+ ]
1215
+ }
1216
+ )
1217
+ ]
1218
+ }
1219
+ );
1220
+ }
1221
+ function Swatches({
1222
+ title,
1223
+ colors,
1224
+ current,
1225
+ onPick,
1226
+ transparentLabel
1227
+ }) {
1228
+ return /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { children: [
1229
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("p", { className: "mb-1 text-[10px] font-medium tracking-wide text-panel-muted-fg uppercase", children: title }),
1230
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { className: "grid grid-cols-8 gap-1", children: [
1231
+ colors.map((color) => /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
1232
+ "button",
1233
+ {
1234
+ type: "button",
1235
+ title: color,
1236
+ onClick: () => onPick(color),
1237
+ className: cn(
1238
+ "aspect-square rounded border border-white/15 outline-none hover:scale-110 focus-visible:ring-2 focus-visible:ring-brand",
1239
+ color.toLowerCase() === current.toLowerCase() && "ring-2 ring-brand"
1240
+ ),
1241
+ style: { background: color }
1242
+ },
1243
+ color
1244
+ )),
1245
+ transparentLabel && /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
1246
+ "button",
1247
+ {
1248
+ type: "button",
1249
+ title: transparentLabel,
1250
+ onClick: () => onPick("transparent"),
1251
+ className: "relative aspect-square overflow-hidden rounded border border-white/15 outline-none hover:scale-110 focus-visible:ring-2 focus-visible:ring-brand",
1252
+ children: /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(Checkerboard, {})
1253
+ }
1254
+ )
1255
+ ] })
1256
+ ] });
1257
+ }
1258
+ function Checkerboard() {
1259
+ return /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
1260
+ "span",
1261
+ {
1262
+ className: "absolute inset-0",
1263
+ style: {
1264
+ backgroundImage: "repeating-conic-gradient(#9ca3af 0% 25%, #e5e7eb 0% 50%)",
1265
+ backgroundSize: "8px 8px"
1266
+ }
1267
+ }
1268
+ );
1269
+ }
1270
+ function toHex(value, { opaque = false } = {}) {
1271
+ const v = value.trim().toLowerCase();
1272
+ const short = /^#([0-9a-f])([0-9a-f])([0-9a-f])$/.exec(v);
1273
+ if (short) return `#${short[1]}${short[1]}${short[2]}${short[2]}${short[3]}${short[3]}`;
1274
+ if (/^#[0-9a-f]{6}$/.test(v)) return v;
1275
+ if (/^#[0-9a-f]{8}$/.test(v)) return opaque || v.endsWith("ff") ? v.slice(0, 7) : v;
1276
+ const rgb = /^rgba?\(\s*(\d+)[\s,]+(\d+)[\s,]+(\d+)(?:[\s,/]+([\d.]+%?))?/.exec(v);
1277
+ if (rgb) {
1278
+ const channels = [rgb[1], rgb[2], rgb[3]].map((n) => Math.min(255, Number(n)));
1279
+ const alphaText = rgb[4];
1280
+ const alpha = alphaText?.endsWith("%") ? parseFloat(alphaText) / 100 : Number(alphaText ?? 1);
1281
+ if (!opaque && alpha < 1) channels.push(Math.round(Math.max(0, alpha) * 255));
1282
+ return `#${channels.map((n) => n.toString(16).padStart(2, "0")).join("")}`;
1283
+ }
1284
+ return null;
1285
+ }
1286
+
1287
+ // src/components/editor/ImageField.tsx
1288
+ var import_lucide_react6 = require("lucide-react");
1289
+ var import_jsx_runtime12 = require("react/jsx-runtime");
1290
+ function ImageField({
1291
+ value,
1292
+ labels,
1293
+ onChoose,
1294
+ onClear
1295
+ }) {
1296
+ const url = imageUrl(value);
1297
+ return /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { className: "relative", children: [
1298
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(
1299
+ "button",
1300
+ {
1301
+ type: "button",
1302
+ onClick: onChoose,
1303
+ title: labels.chooseImage,
1304
+ 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",
1305
+ children: [
1306
+ url ? /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("img", { src: url, alt: "", className: "h-24 w-full bg-black/25 object-contain" }) : /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("span", { className: "flex h-16 flex-col items-center justify-center gap-1 text-xs text-panel-muted-fg", children: [
1307
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(import_lucide_react6.ImageIcon, { className: "size-5" }),
1308
+ labels.chooseImage
1309
+ ] }),
1310
+ url && /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("span", { className: "absolute inset-0 flex items-center justify-center bg-black/50 text-xs font-medium text-white opacity-0 transition group-hover:opacity-100", children: labels.chooseImage })
1311
+ ]
1312
+ }
1313
+ ),
1314
+ value && /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
1315
+ "button",
1316
+ {
1317
+ type: "button",
1318
+ onClick: onClear,
1319
+ title: labels.imageFieldRemove,
1320
+ "aria-label": labels.imageFieldRemove,
1321
+ className: "absolute top-1 right-1 flex size-5 items-center justify-center rounded bg-black/60 text-white hover:bg-black/80",
1322
+ children: /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(import_lucide_react6.X, { className: "size-3" })
1323
+ }
1324
+ )
1325
+ ] });
1326
+ }
1327
+ function imageUrl(value) {
1328
+ const v = value.trim();
1329
+ if (!v || v === "none") return null;
1330
+ const css = /^url\(\s*(['"]?)(.*?)\1\s*\)$/i.exec(v);
1331
+ if (css) return css[2] || null;
1332
+ return /^[a-z-]+\(/i.test(v) ? null : v;
1333
+ }
1334
+
1335
+ // src/components/editor/EmailEditor.tsx
1336
+ var import_client = require("react-dom/client");
1337
+ var import_jsx_runtime13 = require("react/jsx-runtime");
587
1338
  var SEC_ID_RE = /\bsec-([A-Za-z0-9_-]+)\b/;
588
1339
  var OBJ_ID_RE = /\bobj-([A-Za-z0-9_-]+)\b/;
589
1340
  var TYPE_LABEL = {
@@ -596,6 +1347,7 @@ function tagOf(c) {
596
1347
  return String(c.get("tagName") || c.get("type") || "");
597
1348
  }
598
1349
  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>';
1350
+ 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>';
599
1351
  function isSection(c) {
600
1352
  return tagOf(c) === "mj-section";
601
1353
  }
@@ -678,6 +1430,75 @@ function wrapSelectionStyle(el, style) {
678
1430
  } catch {
679
1431
  }
680
1432
  }
1433
+ function setupStyleFields(editor, labels) {
1434
+ const sm = editor.StyleManager;
1435
+ const base = sm.getType("base").view;
1436
+ const documentColors = () => {
1437
+ const source = `${editor.getHtml()} ${editor.getCss() ?? ""}`;
1438
+ const counts = /* @__PURE__ */ new Map();
1439
+ const colorValue = /(?:color|background|border)[\w-]*\s*[:=]\s*["']?[^"';>{}]*?(#[0-9a-f]{3,8}\b|rgba?\([^)]*\))/gi;
1440
+ for (const match of source.matchAll(colorValue)) {
1441
+ const hex = toHex(match[1], { opaque: true });
1442
+ if (hex) counts.set(hex, (counts.get(hex) ?? 0) + 1);
1443
+ }
1444
+ return [...counts.entries()].sort((a, b) => b[1] - a[1]).slice(0, 14).map(([hex]) => hex);
1445
+ };
1446
+ const ownValue = (property) => property.hasValue?.({ noParent: true }) ? String(property.getValue?.() ?? "") : "";
1447
+ const reactView = (field) => base.extend({
1448
+ create({ property }) {
1449
+ const el = document.createElement("div");
1450
+ el.addEventListener("change", (event) => event.stopPropagation());
1451
+ const root = (0, import_client.createRoot)(el);
1452
+ this.__root = root;
1453
+ this.__render = () => root.render(field(property, ownValue(property)));
1454
+ this.__render();
1455
+ return el;
1456
+ },
1457
+ update() {
1458
+ this.__render?.();
1459
+ },
1460
+ destroy() {
1461
+ const root = this.__root;
1462
+ this.__root = void 0;
1463
+ this.__render = void 0;
1464
+ setTimeout(() => root?.unmount());
1465
+ }
1466
+ });
1467
+ sm.addType("color", {
1468
+ view: reactView((property, value) => /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
1469
+ ColorField,
1470
+ {
1471
+ value,
1472
+ placeholder: String(property.getDefaultValue?.() ?? ""),
1473
+ documentColors,
1474
+ labels,
1475
+ onChange: (next, partial) => property.upValue(next, { partial })
1476
+ }
1477
+ ))
1478
+ });
1479
+ sm.addType("file", {
1480
+ view: reactView((property, value) => /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
1481
+ ImageField,
1482
+ {
1483
+ value,
1484
+ labels,
1485
+ onClear: () => property.upValue(""),
1486
+ onChoose: () => {
1487
+ const am = editor.AssetManager;
1488
+ am.open({
1489
+ types: ["image"],
1490
+ current: imageUrl(value) ?? void 0,
1491
+ select(asset, complete) {
1492
+ const url = typeof asset === "string" ? asset : asset.getSrc();
1493
+ property.upValue(url, { partial: !complete });
1494
+ if (complete) am.close();
1495
+ }
1496
+ });
1497
+ }
1498
+ }
1499
+ ))
1500
+ });
1501
+ }
681
1502
  function setupRichText(editor, labels) {
682
1503
  const rte = editor.RichTextEditor;
683
1504
  if (!rte || rte.__customActions) return;
@@ -717,16 +1538,25 @@ function setupRichText(editor, labels) {
717
1538
  function EmailEditor({ docId, onReady, commentsRefresh, onOpenCountChange }) {
718
1539
  const documents = useDocumentStore();
719
1540
  const labels = useLabels();
720
- const loadingRef = (0, import_react5.useRef)(false);
721
- const saveTimer = (0, import_react5.useRef)(null);
722
- const saveStatusRef = (0, import_react5.useRef)("idle");
723
- const stateListeners = (0, import_react5.useRef)(/* @__PURE__ */ new Set());
724
- const [loading, setLoading] = (0, import_react5.useState)(true);
725
- const [sidebarView, setSidebarView] = (0, import_react5.useState)("blocks");
726
- const [composeTarget, setComposeTarget] = (0, import_react5.useState)(null);
727
- const selectionSource = (0, import_react5.useRef)("other");
728
- const editorRef = (0, import_react5.useRef)(null);
729
- const [crumbs, setCrumbs] = (0, import_react5.useState)([]);
1541
+ const loadingRef = (0, import_react7.useRef)(false);
1542
+ const saveStatusRef = (0, import_react7.useRef)("idle");
1543
+ const stateListeners = (0, import_react7.useRef)(/* @__PURE__ */ new Set());
1544
+ const [loading, setLoading] = (0, import_react7.useState)(true);
1545
+ const [sidebarView, setSidebarView] = (0, import_react7.useState)("blocks");
1546
+ const [composeTarget, setComposeTarget] = (0, import_react7.useState)(null);
1547
+ const selectionSource = (0, import_react7.useRef)("other");
1548
+ const editorRef = (0, import_react7.useRef)(null);
1549
+ const { running: turnRunning, locked: readOnly, setLocked } = useAgentTurn();
1550
+ const readOnlyRef = (0, import_react7.useRef)(false);
1551
+ const applyReadOnly = (0, import_react7.useRef)(async () => {
1552
+ });
1553
+ (0, import_react7.useEffect)(() => void applyReadOnly.current(turnRunning), [turnRunning]);
1554
+ (0, import_react7.useEffect)(() => () => setLocked(false), [setLocked]);
1555
+ const [crumbs, setCrumbs] = (0, import_react7.useState)([]);
1556
+ const [selectedImageSrc, setSelectedImageSrc] = (0, import_react7.useState)(null);
1557
+ const [picker, setPicker] = (0, import_react7.useState)(
1558
+ null
1559
+ );
730
1560
  const onEditor = async (editor) => {
731
1561
  editorRef.current = editor;
732
1562
  const buildCrumbs = (c) => {
@@ -753,11 +1583,12 @@ function EmailEditor({ docId, onReady, commentsRefresh, onOpenCountChange }) {
753
1583
  };
754
1584
  const snapshot = () => ({
755
1585
  device: editor.getDevice(),
756
- canUndo: editor.UndoManager.hasUndo(),
757
- canRedo: editor.UndoManager.hasRedo(),
1586
+ canUndo: !readOnlyRef.current && editor.UndoManager.hasUndo(),
1587
+ canRedo: !readOnlyRef.current && editor.UndoManager.hasRedo(),
758
1588
  saveStatus: saveStatusRef.current,
759
1589
  zoom: Math.round(editor.Canvas.getZoom?.() ?? 100),
760
- contentWidth: bodyWidth()
1590
+ contentWidth: bodyWidth(),
1591
+ readOnly: readOnlyRef.current
761
1592
  });
762
1593
  const notifyState = () => {
763
1594
  const s = snapshot();
@@ -781,6 +1612,14 @@ function EmailEditor({ docId, onReady, commentsRefresh, onOpenCountChange }) {
781
1612
  if (isSection(c)) addIdClass(c, "sec");
782
1613
  if (!isCommentable(c)) return;
783
1614
  const toolbar = [...c.get("toolbar") ?? []];
1615
+ if (tagOf(c) === "mj-image" && !toolbar.some((t) => t.command === "open-image-picker")) {
1616
+ toolbar.push({
1617
+ command: "open-image-picker",
1618
+ label: IMAGE_SVG,
1619
+ attributes: { title: labels.chooseImage }
1620
+ });
1621
+ c.set("toolbar", toolbar);
1622
+ }
784
1623
  if (!toolbar.some((t) => t.command === "open-comments")) {
785
1624
  toolbar.push({
786
1625
  command: "open-comments",
@@ -790,6 +1629,21 @@ function EmailEditor({ docId, onReady, commentsRefresh, onOpenCountChange }) {
790
1629
  c.set("toolbar", toolbar);
791
1630
  }
792
1631
  };
1632
+ editor.Commands.add("open-image-picker", {
1633
+ run(ed) {
1634
+ const target = ed.getSelected();
1635
+ if (!target || tagOf(target) !== "mj-image") return;
1636
+ const am = ed.AssetManager;
1637
+ am.open({
1638
+ target,
1639
+ types: ["image"],
1640
+ select(asset, complete) {
1641
+ target.set({ src: asset.getSrc() });
1642
+ if (complete) am.close();
1643
+ }
1644
+ });
1645
+ }
1646
+ });
793
1647
  editor.Commands.add("open-comments", {
794
1648
  run(ed) {
795
1649
  const target = targetOf(ed.getSelected() ?? void 0);
@@ -797,6 +1651,7 @@ function EmailEditor({ docId, onReady, commentsRefresh, onOpenCountChange }) {
797
1651
  }
798
1652
  });
799
1653
  setupRichText(editor, labels);
1654
+ setupStyleFields(editor, labels);
800
1655
  const blockCategory = (label) => {
801
1656
  const l = label.toLowerCase();
802
1657
  if (/(column|section)/.test(l)) return "Layout";
@@ -825,6 +1680,7 @@ function EmailEditor({ docId, onReady, commentsRefresh, onOpenCountChange }) {
825
1680
  if (selectionSource.current === "canvas") setSidebarView("settings");
826
1681
  selectionSource.current = "other";
827
1682
  buildCrumbs(c);
1683
+ setSelectedImageSrc(c && tagOf(c) === "mj-image" ? String(c.get("src") ?? "") : null);
828
1684
  if (c?.get("type") === "mj-image" && !c.getTrait("src")) {
829
1685
  c.addTrait(
830
1686
  {
@@ -838,40 +1694,99 @@ function EmailEditor({ docId, onReady, commentsRefresh, onOpenCountChange }) {
838
1694
  );
839
1695
  }
840
1696
  });
841
- editor.on("component:deselected", () => setCrumbs([]));
1697
+ editor.on("component:deselected", () => {
1698
+ const rest = editor.getSelected();
1699
+ buildCrumbs(rest);
1700
+ setSelectedImageSrc(
1701
+ rest && tagOf(rest) === "mj-image" ? String(rest.get("src") ?? "") : null
1702
+ );
1703
+ });
1704
+ editor.on("component:update:src", (c) => {
1705
+ if (c === editor.getSelected()) setSelectedImageSrc(String(c.get("src") ?? ""));
1706
+ });
1707
+ let stored = "";
842
1708
  const save = async () => {
843
- if (loadingRef.current) return;
1709
+ const view = editor.getEditing()?.getView();
1710
+ await view?.syncContent?.();
1711
+ const mjml = editor.getHtml();
1712
+ if (mjml === stored) return;
844
1713
  setSave("saving");
845
1714
  try {
846
- await documents.save(docId, {
847
- mjml: editor.getHtml(),
848
- projectData: editor.getProjectData()
849
- });
1715
+ await documents.save(docId, { mjml, projectData: editor.getProjectData() });
1716
+ stored = mjml;
850
1717
  setSave("saved");
851
1718
  } catch (e) {
852
1719
  setSave("error");
853
- import_sonner.toast.error(labels.documentSaveFailed);
1720
+ import_sonner2.toast.error(refusal(e) ?? labels.documentSaveFailed, { id: saveToastId(docId) });
854
1721
  throw e;
855
1722
  }
856
1723
  };
1724
+ const queue = createSaveQueue(save, 1200);
857
1725
  editor.on("update", () => {
858
1726
  notifyState();
859
- if (loadingRef.current) return;
860
- if (saveTimer.current) clearTimeout(saveTimer.current);
861
- saveTimer.current = setTimeout(() => void save().catch(console.error), 1200);
1727
+ if (!loadingRef.current && !readOnlyRef.current) queue.schedule();
862
1728
  });
863
1729
  editor.on("change:device", notifyState);
1730
+ const blocked = [
1731
+ "core:undo",
1732
+ "core:redo",
1733
+ "core:paste",
1734
+ "core:component-delete",
1735
+ "tlb-delete",
1736
+ "tlb-clone",
1737
+ "tlb-move",
1738
+ "open-assets",
1739
+ "open-image-picker"
1740
+ ];
1741
+ for (const id of blocked) {
1742
+ editor.on(`command:run:before:${id}`, ({ options }) => {
1743
+ if (readOnlyRef.current) options.abort = true;
1744
+ });
1745
+ }
1746
+ const lock = (on) => {
1747
+ readOnlyRef.current = on;
1748
+ setLocked(on);
1749
+ notifyState();
1750
+ };
1751
+ applyReadOnly.current = async (on) => {
1752
+ if (on) {
1753
+ lock(true);
1754
+ queue.cancel();
1755
+ editor.select([]);
1756
+ editor.AssetManager.close();
1757
+ setPicker(null);
1758
+ return;
1759
+ }
1760
+ try {
1761
+ await reload();
1762
+ } catch (e) {
1763
+ console.error(e);
1764
+ import_sonner2.toast.error(labels.documentLoadFailed, {
1765
+ id: `reload-${docId}`,
1766
+ duration: Infinity,
1767
+ action: { label: labels.retry, onClick: () => void applyReadOnly.current(false) }
1768
+ });
1769
+ return;
1770
+ }
1771
+ lock(false);
1772
+ };
1773
+ const reload = () => queue.run(async () => {
1774
+ const fresh = await documents.get(docId);
1775
+ if (fresh.mjml !== stored) loadMjml(fresh.mjml);
1776
+ });
864
1777
  const loadMjml = (mjml) => {
865
1778
  loadingRef.current = true;
866
- if (saveTimer.current) clearTimeout(saveTimer.current);
867
- editor.setComponents(mjml || import_core2.STARTER_MJML);
868
- editor.getWrapper()?.find("mj-section").forEach(decorate);
869
- for (const t of Object.keys(TYPE_LABEL)) {
870
- editor.getWrapper()?.find(t).forEach(decorate);
871
- }
872
- setTimeout(() => {
1779
+ try {
1780
+ queue.cancel();
1781
+ stored = mjml;
1782
+ editor.setComponents(mjml || import_core2.STARTER_MJML);
1783
+ editor.getWrapper()?.find("mj-section").forEach(decorate);
1784
+ for (const t of Object.keys(TYPE_LABEL)) {
1785
+ editor.getWrapper()?.find(t).forEach(decorate);
1786
+ }
1787
+ } finally {
873
1788
  loadingRef.current = false;
874
- }, 400);
1789
+ }
875
1790
  };
876
1791
  const ensureHighlightStyles = () => {
877
1792
  const cdoc = editor.Canvas.getDocument();
@@ -908,7 +1823,7 @@ function EmailEditor({ docId, onReady, commentsRefresh, onOpenCountChange }) {
908
1823
  const doc = await documents.get(docId);
909
1824
  loadMjml(doc.mjml);
910
1825
  } catch (e) {
911
- import_sonner.toast.error(labels.documentLoadFailed);
1826
+ import_sonner2.toast.error(labels.documentLoadFailed);
912
1827
  console.error(e);
913
1828
  } finally {
914
1829
  setLoading(false);
@@ -916,14 +1831,8 @@ function EmailEditor({ docId, onReady, commentsRefresh, onOpenCountChange }) {
916
1831
  onReady({
917
1832
  getMjml: () => editor.getHtml(),
918
1833
  getCompiledHtml: compiledHtml,
919
- flushSave: async () => {
920
- if (saveTimer.current) clearTimeout(saveTimer.current);
921
- await save();
922
- },
923
- reloadFromDb: async () => {
924
- const fresh = await documents.get(docId);
925
- if (fresh.mjml !== editor.getHtml()) loadMjml(fresh.mjml);
926
- },
1834
+ flushSave: () => queue.flush(),
1835
+ reloadFromDb: reload,
927
1836
  highlightSection: (sectionId, on) => {
928
1837
  ensureHighlightStyles();
929
1838
  const el = sectionEl(sectionId);
@@ -939,10 +1848,12 @@ function EmailEditor({ docId, onReady, commentsRefresh, onOpenCountChange }) {
939
1848
  notifyState();
940
1849
  },
941
1850
  undo: () => {
1851
+ if (readOnlyRef.current) return;
942
1852
  editor.UndoManager.undo();
943
1853
  notifyState();
944
1854
  },
945
1855
  redo: () => {
1856
+ if (readOnlyRef.current) return;
946
1857
  editor.UndoManager.redo();
947
1858
  notifyState();
948
1859
  },
@@ -951,6 +1862,7 @@ function EmailEditor({ docId, onReady, commentsRefresh, onOpenCountChange }) {
951
1862
  notifyState();
952
1863
  },
953
1864
  setContentWidth: (w) => {
1865
+ if (readOnlyRef.current) return;
954
1866
  findByTag("mj-body")?.addAttributes({ width: w });
955
1867
  notifyState();
956
1868
  },
@@ -961,8 +1873,8 @@ function EmailEditor({ docId, onReady, commentsRefresh, onOpenCountChange }) {
961
1873
  }
962
1874
  });
963
1875
  };
964
- return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
965
- import_react4.default,
1876
+ return /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
1877
+ import_react6.default,
966
1878
  {
967
1879
  grapesjs: import_grapesjs.default,
968
1880
  className: "h-full",
@@ -975,6 +1887,19 @@ function EmailEditor({ docId, onReady, commentsRefresh, onOpenCountChange }) {
975
1887
  styleManager: { appendTo: "#gjs-styles" },
976
1888
  traitManager: { appendTo: "#gjs-traits" },
977
1889
  layerManager: { appendTo: "#gjs-layers" },
1890
+ assetManager: {
1891
+ // Never inline files: mail clients block `data:` images (see ImageLibrary).
1892
+ embedAsBase64: false,
1893
+ // Our own dialog instead of GrapesJS's modal. GrapesJS still decides when it opens
1894
+ // and what a choice applies to; `select(url, true)` sets the image and closes.
1895
+ custom: {
1896
+ open: (props) => setPicker({
1897
+ current: props.options?.current ?? (String(props.options?.target?.get("src") ?? "") || void 0),
1898
+ select: (url) => props.select(url, true)
1899
+ }),
1900
+ close: () => setPicker(null)
1901
+ }
1902
+ },
978
1903
  deviceManager: {
979
1904
  devices: [
980
1905
  { id: "desktop", name: "Desktop", width: "" },
@@ -984,19 +1909,26 @@ function EmailEditor({ docId, onReady, commentsRefresh, onOpenCountChange }) {
984
1909
  plugins: [import_grapesjs_mjml.default]
985
1910
  },
986
1911
  onEditor,
987
- children: /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { className: "flex h-full min-h-0 flex-1", children: [
988
- /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
1912
+ children: /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("div", { className: "relative flex h-full min-h-0 flex-1", inert: readOnly, children: [
1913
+ readOnly && /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("div", { className: "absolute inset-0 z-30 flex cursor-not-allowed items-start justify-center bg-white/20 pt-3", children: /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("span", { className: "flex items-center gap-2 rounded-full bg-panel px-3 py-1 text-xs text-panel-fg shadow", children: [
1914
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(Spinner, {}),
1915
+ " ",
1916
+ labels.agentWorking
1917
+ ] }) }),
1918
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
989
1919
  LeftSidebar,
990
1920
  {
991
1921
  view: sidebarView,
992
1922
  onViewChange: setSidebarView,
993
- markLayersSource: () => selectionSource.current = "layers"
1923
+ markLayersSource: () => selectionSource.current = "layers",
1924
+ selectedImageSrc,
1925
+ onChooseImage: () => editorRef.current?.runCommand("open-image-picker")
994
1926
  }
995
1927
  ),
996
- /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { className: "flex min-w-0 flex-1 flex-col bg-surface-muted", children: [
997
- /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { className: "relative min-h-0 flex-1", children: [
998
- /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(import_react4.Canvas, { className: "h-full" }),
999
- /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
1928
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("div", { className: "flex min-w-0 flex-1 flex-col bg-surface-muted", children: [
1929
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("div", { className: "relative min-h-0 flex-1", children: [
1930
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(import_react6.Canvas, { className: "h-full" }),
1931
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
1000
1932
  CanvasComments,
1001
1933
  {
1002
1934
  docId,
@@ -1006,12 +1938,27 @@ function EmailEditor({ docId, onReady, commentsRefresh, onOpenCountChange }) {
1006
1938
  onOpenCountChange
1007
1939
  }
1008
1940
  ),
1009
- loading && /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { className: "absolute inset-0 z-10 flex items-center justify-center gap-2 bg-white/70 text-sm text-zinc-500", children: [
1010
- /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(Spinner, {}),
1941
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
1942
+ ImagePicker,
1943
+ {
1944
+ open: picker !== null,
1945
+ currentUrl: picker?.current,
1946
+ onSelect: (url) => {
1947
+ if (!readOnlyRef.current) picker?.select(url);
1948
+ setPicker(null);
1949
+ },
1950
+ onClose: () => {
1951
+ setPicker(null);
1952
+ editorRef.current?.AssetManager.close();
1953
+ }
1954
+ }
1955
+ ),
1956
+ loading && /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("div", { className: "absolute inset-0 z-10 flex items-center justify-center gap-2 bg-white/70 text-sm text-zinc-500", children: [
1957
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(Spinner, {}),
1011
1958
  " Loading editor\u2026"
1012
1959
  ] })
1013
1960
  ] }),
1014
- /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(Breadcrumb, { crumbs, onSelect: (c) => editorRef.current?.select(c) })
1961
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(Breadcrumb, { crumbs, onSelect: (c) => editorRef.current?.select(c) })
1015
1962
  ] })
1016
1963
  ] })
1017
1964
  }
@@ -1022,32 +1969,34 @@ function CollapseSection({
1022
1969
  defaultOpen = true,
1023
1970
  children
1024
1971
  }) {
1025
- const [open, setOpen] = (0, import_react5.useState)(defaultOpen);
1026
- return /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { className: "border-b border-panel-border", children: [
1027
- /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(
1972
+ const [open, setOpen] = (0, import_react7.useState)(defaultOpen);
1973
+ return /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("div", { className: "border-b border-panel-border", children: [
1974
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)(
1028
1975
  "button",
1029
1976
  {
1030
1977
  type: "button",
1031
1978
  onClick: () => setOpen((o) => !o),
1032
1979
  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",
1033
1980
  children: [
1034
- /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(import_lucide_react3.ChevronRight, { className: cn("size-3.5 transition-transform", open && "rotate-90") }),
1981
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(import_lucide_react7.ChevronRight, { className: cn("size-3.5 transition-transform", open && "rotate-90") }),
1035
1982
  title
1036
1983
  ]
1037
1984
  }
1038
1985
  ),
1039
- /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { className: cn(!open && "hidden"), children })
1986
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("div", { className: cn(!open && "hidden"), children })
1040
1987
  ] });
1041
1988
  }
1042
1989
  function LeftSidebar({
1043
1990
  view,
1044
1991
  onViewChange,
1045
- markLayersSource
1992
+ markLayersSource,
1993
+ selectedImageSrc,
1994
+ onChooseImage
1046
1995
  }) {
1047
- const editor = (0, import_react4.useEditorMaybe)();
1996
+ const editor = (0, import_react6.useEditorMaybe)();
1048
1997
  const labels = useLabels();
1049
- const [layersExpanded, setLayersExpanded] = (0, import_react5.useState)(false);
1050
- const [blockSearch, setBlockSearch] = (0, import_react5.useState)("");
1998
+ const [layersExpanded, setLayersExpanded] = (0, import_react7.useState)(false);
1999
+ const [blockSearch, setBlockSearch] = (0, import_react7.useState)("");
1051
2000
  const changeView = (v) => {
1052
2001
  if (v === "settings" && editor && !editor.getSelected()) {
1053
2002
  const wrapper = editor.getWrapper();
@@ -1076,12 +2025,12 @@ function LeftSidebar({
1076
2025
  cat.style.display = anyVisible ? "" : "none";
1077
2026
  });
1078
2027
  };
1079
- return /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { className: "editor-dark flex w-64 shrink-0 flex-col border-r border-panel-border bg-panel text-panel-fg", children: [
1080
- /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { className: "flex border-b border-panel-border", children: [
2028
+ return /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("div", { className: "editor-dark flex w-64 shrink-0 flex-col border-r border-panel-border bg-panel text-panel-fg", children: [
2029
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("div", { className: "flex border-b border-panel-border", children: [
1081
2030
  ["blocks", "Blocks"],
1082
2031
  ["settings", "Settings"],
1083
2032
  ["layers", "Layers"]
1084
- ].map(([value, label]) => /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
2033
+ ].map(([value, label]) => /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
1085
2034
  "button",
1086
2035
  {
1087
2036
  type: "button",
@@ -1095,10 +2044,10 @@ function LeftSidebar({
1095
2044
  },
1096
2045
  value
1097
2046
  )) }),
1098
- /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { className: cn("flex min-h-0 flex-1 flex-col", view !== "blocks" && "hidden"), children: [
1099
- /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { className: "relative border-b border-panel-border p-2", children: [
1100
- /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(import_lucide_react4.Search, { className: "pointer-events-none absolute top-1/2 left-4 size-3.5 -translate-y-1/2 text-panel-muted-fg" }),
1101
- /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
2047
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("div", { className: cn("flex min-h-0 flex-1 flex-col", view !== "blocks" && "hidden"), children: [
2048
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("div", { className: "relative border-b border-panel-border p-2", children: [
2049
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(import_lucide_react8.Search, { className: "pointer-events-none absolute top-1/2 left-4 size-3.5 -translate-y-1/2 text-panel-muted-fg" }),
2050
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
1102
2051
  Input,
1103
2052
  {
1104
2053
  value: blockSearch,
@@ -1107,23 +2056,58 @@ function LeftSidebar({
1107
2056
  className: "h-8 border-panel-border bg-panel-elevated pr-10 pl-7 text-sm text-panel-fg placeholder:text-panel-muted-fg"
1108
2057
  }
1109
2058
  ),
1110
- /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("kbd", { className: "pointer-events-none absolute top-1/2 right-4 -translate-y-1/2 rounded border border-panel-border bg-panel px-1 py-0.5 font-mono text-[10px] text-panel-muted-fg", children: "\u2318K" })
2059
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("kbd", { className: "pointer-events-none absolute top-1/2 right-4 -translate-y-1/2 rounded border border-panel-border bg-panel px-1 py-0.5 font-mono text-[10px] text-panel-muted-fg", children: "\u2318K" })
1111
2060
  ] }),
1112
- /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { id: "gjs-blocks", className: "min-h-0 flex-1 overflow-y-auto" })
2061
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("div", { id: "gjs-blocks", className: "min-h-0 flex-1 overflow-y-auto" })
1113
2062
  ] }),
1114
- /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { className: cn("min-h-0 flex-1 overflow-y-auto", view !== "settings" && "hidden"), children: [
1115
- /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(CollapseSection, { title: labels.attributes, children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { id: "gjs-traits" }) }),
1116
- /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(CollapseSection, { title: labels.style, children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { id: "gjs-styles" }) })
2063
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("div", { className: cn("min-h-0 flex-1 overflow-y-auto", view !== "settings" && "hidden"), children: [
2064
+ selectedImageSrc !== null && /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("div", { className: "border-b border-panel-border p-3", children: [
2065
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)(
2066
+ "button",
2067
+ {
2068
+ type: "button",
2069
+ onClick: onChooseImage,
2070
+ title: labels.chooseImage,
2071
+ className: "group relative block w-full overflow-hidden rounded-lg border border-panel-border bg-panel-elevated",
2072
+ children: [
2073
+ selectedImageSrc ? /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
2074
+ "img",
2075
+ {
2076
+ src: selectedImageSrc,
2077
+ alt: "",
2078
+ className: "h-32 w-full bg-black/25 object-contain"
2079
+ }
2080
+ ) : /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("span", { className: "flex h-32 items-center justify-center text-panel-muted-fg", children: /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(import_lucide_react7.ImageIcon, { className: "size-8" }) }),
2081
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("span", { className: "absolute inset-0 flex items-center justify-center bg-black/50 text-sm font-medium text-white opacity-0 transition group-hover:opacity-100", children: labels.chooseImage })
2082
+ ]
2083
+ }
2084
+ ),
2085
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)(
2086
+ Button,
2087
+ {
2088
+ type: "button",
2089
+ size: "sm",
2090
+ className: "mt-2 w-full bg-brand text-brand-fg hover:bg-brand/90",
2091
+ onClick: onChooseImage,
2092
+ children: [
2093
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(import_lucide_react7.ImageUp, {}),
2094
+ labels.chooseImage
2095
+ ]
2096
+ }
2097
+ )
2098
+ ] }),
2099
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(CollapseSection, { title: labels.attributes, children: /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("div", { id: "gjs-traits" }) }),
2100
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(CollapseSection, { title: labels.style, children: /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("div", { id: "gjs-styles" }) })
1117
2101
  ] }),
1118
- /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(
2102
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)(
1119
2103
  "div",
1120
2104
  {
1121
2105
  className: cn("flex min-h-0 flex-1 flex-col", view !== "layers" && "hidden"),
1122
2106
  onMouseDownCapture: markLayersSource,
1123
2107
  children: [
1124
- /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("label", { className: "flex items-center justify-between gap-2 border-b border-panel-border px-3 py-2 text-xs text-panel-muted-fg", children: [
1125
- /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { children: labels.expandAll }),
1126
- /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
2108
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("label", { className: "flex items-center justify-between gap-2 border-b border-panel-border px-3 py-2 text-xs text-panel-muted-fg", children: [
2109
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("span", { children: labels.expandAll }),
2110
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
1127
2111
  Switch,
1128
2112
  {
1129
2113
  checked: layersExpanded,
@@ -1132,7 +2116,7 @@ function LeftSidebar({
1132
2116
  }
1133
2117
  )
1134
2118
  ] }),
1135
- /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { id: "gjs-layers", className: "min-h-0 flex-1 overflow-y-auto" })
2119
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("div", { id: "gjs-layers", className: "min-h-0 flex-1 overflow-y-auto" })
1136
2120
  ]
1137
2121
  }
1138
2122
  )
@@ -1143,9 +2127,9 @@ function Breadcrumb({
1143
2127
  onSelect
1144
2128
  }) {
1145
2129
  const labels = useLabels();
1146
- return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { className: "flex h-9 shrink-0 items-center gap-0.5 border-t border-border bg-surface px-3 text-xs text-muted-foreground", children: crumbs.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { className: "text-muted-foreground/60", children: labels.selectAnElement }) : crumbs.map((c, i) => /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("span", { className: "flex items-center gap-0.5", children: [
1147
- i > 0 && /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(import_lucide_react3.ChevronRight, { className: "size-3 opacity-40" }),
1148
- /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
2130
+ return /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("div", { className: "flex h-9 shrink-0 items-center gap-0.5 border-t border-border bg-surface px-3 text-xs text-muted-foreground", children: crumbs.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("span", { className: "text-muted-foreground/60", children: labels.selectAnElement }) : crumbs.map((c, i) => /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("span", { className: "flex items-center gap-0.5", children: [
2131
+ i > 0 && /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(import_lucide_react7.ChevronRight, { className: "size-3 opacity-40" }),
2132
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
1149
2133
  "button",
1150
2134
  {
1151
2135
  type: "button",